@hover-dev/core 0.10.0 → 0.12.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.
@@ -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":"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"}
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
@@ -13,6 +13,7 @@
13
13
  * { type: 'skill-saved', payload: { name, path } }
14
14
  * { type: 'skill-exists', payload: { slug, existingPath } }
15
15
  * { type: 'skills-list', payload: { skills: SkillSummary[] } }
16
+ * { type: 'specs-list', payload: { specs: SpecSummary[] } }
16
17
  * { type: 'spec-saved', payload: { name, path } }
17
18
  * { type: 'spec-exists', payload: { slug, existingPath } }
18
19
  * { type: 'case-csv-saved', payload: { name, path } }
@@ -20,7 +21,12 @@
20
21
  * { type: 'error', payload: { message } }
21
22
  *
22
23
  * client → server
23
- * { type: 'command', payload: { text, sessionId? } }
24
+ * { type: 'command', payload: { text, sessionId?, reRecord?: { slug } } }
25
+ * // when reRecord.slug is set, the
26
+ * // service collects tool_use events
27
+ * // into a step list and on a clean
28
+ * // session_end overwrites
29
+ * // __vibe_tests__/<slug>.spec.ts
24
30
  * { type: 'cancel' }
25
31
  * { type: 'check-cdp', payload: { pageUrl } } // "is this widget in the debug Chrome?"
26
32
  * { type: 'launch-chrome', payload: { pageUrl } } // start debug Chrome, navigate to pageUrl
@@ -29,6 +35,7 @@
29
35
  * { type: 'save-spec', payload: { name, description, steps, assertions?, overwrite? } }
30
36
  * { type: 'save-case-csv', payload: { name, description, steps, assertions?, jiraProjectKey?, labels?, overwrite? } }
31
37
  * { type: 'list-skills' }
38
+ * { type: 'list-specs' } // ask for every spec under __vibe_tests__/, with parsed JSDoc headers
32
39
  * { type: 'list-agents' } // ask for the full agent registry + install status
33
40
  * { type: 'switch-agent', payload: { agentId } } // set the service's current agent; broadcasts to all connections
34
41
  *
@@ -48,6 +55,7 @@ import { getAgent } from './agents/registry.js';
48
55
  import { getPreflight, invalidatePreflight } from './playwright/preflightCache.js';
49
56
  import { resolveMcpConfig } from './playwright/resolveMcpConfig.js';
50
57
  import { listSkills } from './skills/writeSkill.js';
58
+ import { listSpecs } from './specs/listSpecs.js';
51
59
  import { send, sendIfOpen } from './service/types.js';
52
60
  import { buildCdpHint, buildCdpHintResume } from './service/cdpHint.js';
53
61
  import { handleCheckCdp, handleLaunchChrome, handleFocusDebug, } from './service/cdpHandlers.js';
@@ -525,6 +533,15 @@ export async function startService(opts) {
525
533
  send(ws, { type: 'skills-list', payload: { skills } });
526
534
  return;
527
535
  }
536
+ if (msg.type === 'list-specs') {
537
+ // Widget asks for every spec under <devRoot>/__vibe_tests__/ so it
538
+ // can render the Specs tab in the Saved-sessions overlay. Each
539
+ // summary carries `originalPrompt` (parsed from the JSDoc header)
540
+ // so the Re-record button can resubmit it as a normal command.
541
+ const specs = await listSpecs(devRoot);
542
+ send(ws, { type: 'specs-list', payload: { specs } });
543
+ return;
544
+ }
528
545
  if (msg.type === 'save-spec') {
529
546
  await handleSaveArtifact(ws, msg, devRoot, SPEC_CONFIG);
530
547
  return;
@@ -533,6 +550,39 @@ export async function startService(opts) {
533
550
  await handleSaveArtifact(ws, msg, devRoot, CASE_CSV_CONFIG);
534
551
  return;
535
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
+ }
536
586
  if (msg.type === 'check-cdp') {
537
587
  await handleCheckCdp(ws, msg, cdpUrl, effectiveLaunchExtras());
538
588
  return;
@@ -551,6 +601,15 @@ export async function startService(opts) {
551
601
  const resumeSessionId = typeof msg.payload?.sessionId === 'string' && msg.payload.sessionId.length > 0
552
602
  ? msg.payload.sessionId
553
603
  : undefined;
604
+ // Re-record mode: when the client (widget Specs tab or hover CLI)
605
+ // passes `reRecord: { slug }`, we collect tool_use events server-side
606
+ // into a SkillStep[] and, on session_end with no error, overwrite the
607
+ // existing __vibe_tests__/<slug>.spec.ts. This is the same flow the
608
+ // widget uses for "Save as Spec", but the spec already exists and is
609
+ // being regenerated for the current UI.
610
+ const reRecordSlug = msg.payload && typeof msg.payload === 'object' && 'reRecord' in msg.payload
611
+ ? msg.payload.reRecord?.slug
612
+ : undefined;
554
613
  if (typeof text !== 'string' || !text.trim())
555
614
  return;
556
615
  if (busy) {
@@ -563,6 +622,16 @@ export async function startService(opts) {
563
622
  busy = true;
564
623
  cancelled = false;
565
624
  inflight = new AbortController();
625
+ // Re-record step collector — populated as tool_use events stream by,
626
+ // consumed at session_end to overwrite the original spec. Empty unless
627
+ // reRecordSlug is set on this command. We seed with a synthetic
628
+ // `user` step so writeSpec's JSDoc Original-prompt: line carries the
629
+ // text the agent was actually given (which is the prompt we read out
630
+ // of the existing spec — the same one we're regenerating).
631
+ const reRecordSteps = [];
632
+ if (reRecordSlug) {
633
+ reRecordSteps.push({ kind: 'user', text });
634
+ }
566
635
  try {
567
636
  // Build the MCP config first — it's pure local file IO and lets
568
637
  // us assert plugin-contributed servers landed in the config even
@@ -684,6 +753,61 @@ export async function startService(opts) {
684
753
  if (cancelled || ws.readyState !== WebSocket.OPEN)
685
754
  return;
686
755
  send(ws, { type: 'event', payload: ev });
756
+ // Re-record collection. Mirror what widget client.js does on the
757
+ // way past tool_use events: accumulate into a SkillStep[] so we
758
+ // can write a fresh spec when the session ends. We do this only
759
+ // when this command was launched in re-record mode; ordinary
760
+ // commands don't need server-side step retention (widget owns
761
+ // that for normal saves).
762
+ if (reRecordSlug && ev.kind === 'tool_use') {
763
+ reRecordSteps.push({
764
+ kind: 'step',
765
+ tool: ev.tool,
766
+ input: ev.input,
767
+ });
768
+ }
769
+ if (reRecordSlug && ev.kind === 'session_end') {
770
+ // Cancelled or errored runs: don't overwrite — the existing
771
+ // spec is still valid. Tell the client what happened.
772
+ if (ev.isError) {
773
+ sendIfOpen(ws, {
774
+ type: 'error',
775
+ payload: {
776
+ message: `Re-record failed: ${ev.summary ?? 'agent reported an error'}. ` +
777
+ `Original spec left unchanged.`,
778
+ },
779
+ });
780
+ }
781
+ else {
782
+ // Snapshot the agent's final summary into a synthetic `done`
783
+ // step so writeSpec's `Outcome:` header reflects the new run.
784
+ if (ev.summary) {
785
+ reRecordSteps.push({ kind: 'done', summary: ev.summary });
786
+ }
787
+ // Overwrite. writeSpec uses the slug to name the file; we
788
+ // pass the original slug verbatim so the path is stable.
789
+ try {
790
+ const { writeSpec } = await import('./specs/writeSpec.js');
791
+ const result = await writeSpec({
792
+ devRoot,
793
+ name: reRecordSlug,
794
+ steps: reRecordSteps,
795
+ overwrite: true,
796
+ });
797
+ sendIfOpen(ws, {
798
+ type: 'spec-saved',
799
+ payload: { name: reRecordSlug, path: result.path },
800
+ });
801
+ }
802
+ catch (e) {
803
+ const m = e instanceof Error ? e.message : String(e);
804
+ sendIfOpen(ws, {
805
+ type: 'error',
806
+ payload: { message: `Re-record could not write spec: ${m}` },
807
+ });
808
+ }
809
+ }
810
+ }
687
811
  }
688
812
  }
689
813
  catch (err) {
@@ -0,0 +1,40 @@
1
+ export interface SpecSummary {
2
+ /** Path-relative slug, e.g. `login-and-counter`. Identifies the spec. */
3
+ slug: string;
4
+ /** Absolute path to the .spec.ts file. */
5
+ path: string;
6
+ /** `Original prompt:` parsed from the JSDoc header. `null` for
7
+ * hand-authored specs that have no header — they list but can't be
8
+ * re-recorded automatically. */
9
+ originalPrompt: string | null;
10
+ /** First line of `Outcome:` from the JSDoc header, if present. */
11
+ outcome: string | null;
12
+ /** Number of `Steps:` lines parsed (informational only). */
13
+ stepCount: number;
14
+ /** File mtime in ms — used to show "saved 2 hours ago" in the UI. */
15
+ mtimeMs: number;
16
+ }
17
+ export interface SpecHeader {
18
+ /** Raw text of `Original prompt:` line, or null when absent. */
19
+ originalPrompt: string | null;
20
+ /** First line of `Outcome:`. */
21
+ outcome: string | null;
22
+ /** Step lines from the `Steps:` block, in order. */
23
+ steps: string[];
24
+ /** Lines from the `Expected:` block, in order. */
25
+ expected: string[];
26
+ }
27
+ /**
28
+ * Parse the JSDoc header that `writeSpec.ts` emits. Tolerant of:
29
+ * - Specs without any JSDoc (returns all-null).
30
+ * - Hand-edited specs where users reordered or trimmed sections.
31
+ * - Long prompts that wrap across lines (we take only the first line).
32
+ */
33
+ export declare function parseSpecHeader(source: string): SpecHeader;
34
+ /**
35
+ * List every `*.spec.ts` file under `<devRoot>/__vibe_tests__/` with its
36
+ * parsed header. Returns newest-first by mtime so the widget overlay shows
37
+ * recently-saved specs at the top.
38
+ */
39
+ export declare function listSpecs(devRoot: string): Promise<SpecSummary[]>;
40
+ //# sourceMappingURL=listSpecs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"listSpecs.d.ts","sourceRoot":"","sources":["../../src/specs/listSpecs.ts"],"names":[],"mappings":"AAkBA,MAAM,WAAW,WAAW;IAC1B,yEAAyE;IACzE,IAAI,EAAE,MAAM,CAAC;IACb,0CAA0C;IAC1C,IAAI,EAAE,MAAM,CAAC;IACb;;qCAEiC;IACjC,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,kEAAkE;IAClE,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,4DAA4D;IAC5D,SAAS,EAAE,MAAM,CAAC;IAClB,qEAAqE;IACrE,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,UAAU;IACzB,gEAAgE;IAChE,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,gCAAgC;IAChC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,oDAAoD;IACpD,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,kDAAkD;IAClD,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,UAAU,CAsB1D;AA4BD;;;;GAIG;AACH,wBAAsB,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC,CAkCvE"}
@@ -0,0 +1,114 @@
1
+ /**
2
+ * List + parse Hover-generated Playwright specs under `<devRoot>/__vibe_tests__/`.
3
+ *
4
+ * Used by:
5
+ * - The widget's "Specs" overlay tab (server pushes a SpecSummary[] list).
6
+ * - The CLI's `hover re-record <spec>` subcommand (parses one spec for its
7
+ * `Original prompt:` JSDoc header).
8
+ *
9
+ * Hand-authored specs (no Hover JSDoc header) are listed but reported with
10
+ * `originalPrompt: null` — the UI / CLI surfaces that "this spec can't be
11
+ * re-recorded automatically; the natural-language intent isn't recorded."
12
+ *
13
+ * Mirrors the listSkills shape so widget UI can use the same row renderer.
14
+ */
15
+ import { readdir, readFile } from 'node:fs/promises';
16
+ import { stat } from 'node:fs/promises';
17
+ import { join } from 'node:path';
18
+ /**
19
+ * Parse the JSDoc header that `writeSpec.ts` emits. Tolerant of:
20
+ * - Specs without any JSDoc (returns all-null).
21
+ * - Hand-edited specs where users reordered or trimmed sections.
22
+ * - Long prompts that wrap across lines (we take only the first line).
23
+ */
24
+ export function parseSpecHeader(source) {
25
+ // JSDoc block right after the @playwright/test import (or at file top).
26
+ // We don't require it to be the very first JSDoc — there could be a
27
+ // banner comment from a linter. We DO require it to appear before the
28
+ // first `test(` / `test.describe(` so that long file footers can't
29
+ // confuse the parser.
30
+ const beforeFirstTest = source.split(/^\s*(?:test|test\.describe)\s*\(/m)[0] ?? source;
31
+ const blockMatch = beforeFirstTest.match(/\/\*\*([\s\S]*?)\*\//);
32
+ if (!blockMatch) {
33
+ return { originalPrompt: null, outcome: null, steps: [], expected: [] };
34
+ }
35
+ const block = blockMatch[1];
36
+ const originalPrompt = extractScalar(block, /^\s*\*\s*Original prompt:\s*(.+?)\s*$/m);
37
+ const outcome = extractScalar(block, /^\s*\*\s*Outcome:\s*(.+?)\s*$/m);
38
+ return {
39
+ originalPrompt,
40
+ outcome,
41
+ steps: extractList(block, /^\s*\*\s*Steps:\s*$/m),
42
+ expected: extractList(block, /^\s*\*\s*Expected:\s*$/m),
43
+ };
44
+ }
45
+ function extractScalar(block, re) {
46
+ const m = block.match(re);
47
+ return m ? m[1].trim() : null;
48
+ }
49
+ /**
50
+ * Extract a JSDoc list-style block. Given a header regex matching "Steps:"
51
+ * or "Expected:", read subsequent ` * <indented line>` lines until the next
52
+ * top-level marker (blank ` *` line or another `Section:` header).
53
+ */
54
+ function extractList(block, headerRe) {
55
+ const match = block.match(headerRe);
56
+ if (!match)
57
+ return [];
58
+ const start = (match.index ?? 0) + match[0].length;
59
+ const tail = block.slice(start);
60
+ const lines = [];
61
+ for (const raw of tail.split('\n')) {
62
+ // Stop at a blank JSDoc line (` *` only) or another `Section:` header.
63
+ if (/^\s*\*\s*$/.test(raw))
64
+ break;
65
+ if (/^\s*\*\s*\w[\w ]*:\s*$/.test(raw) || /^\s*\*\s*\w[\w ]*:\s/.test(raw))
66
+ break;
67
+ const m = raw.match(/^\s*\*\s*(?:[•\-\*\d.]\s*)*(.+?)\s*$/);
68
+ if (m && m[1])
69
+ lines.push(m[1]);
70
+ }
71
+ return lines;
72
+ }
73
+ /**
74
+ * List every `*.spec.ts` file under `<devRoot>/__vibe_tests__/` with its
75
+ * parsed header. Returns newest-first by mtime so the widget overlay shows
76
+ * recently-saved specs at the top.
77
+ */
78
+ export async function listSpecs(devRoot) {
79
+ const root = join(devRoot, '__vibe_tests__');
80
+ let entries;
81
+ try {
82
+ entries = await readdir(root);
83
+ }
84
+ catch {
85
+ return [];
86
+ }
87
+ const summaries = [];
88
+ for (const entry of entries) {
89
+ if (!entry.endsWith('.spec.ts'))
90
+ continue;
91
+ const path = join(root, entry);
92
+ let content;
93
+ let mtimeMs = 0;
94
+ try {
95
+ content = await readFile(path, 'utf-8');
96
+ const st = await stat(path);
97
+ mtimeMs = st.mtimeMs;
98
+ }
99
+ catch {
100
+ continue;
101
+ }
102
+ const header = parseSpecHeader(content);
103
+ summaries.push({
104
+ slug: entry.replace(/\.spec\.ts$/, ''),
105
+ path,
106
+ originalPrompt: header.originalPrompt,
107
+ outcome: header.outcome,
108
+ stepCount: header.steps.length,
109
+ mtimeMs,
110
+ });
111
+ }
112
+ summaries.sort((a, b) => b.mtimeMs - a.mtimeMs);
113
+ return summaries;
114
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hover-dev/core",
3
- "version": "0.10.0",
3
+ "version": "0.12.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",