@hover-dev/core 0.10.0 → 0.11.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.
@@ -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,CAswB/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;
@@ -551,6 +568,15 @@ export async function startService(opts) {
551
568
  const resumeSessionId = typeof msg.payload?.sessionId === 'string' && msg.payload.sessionId.length > 0
552
569
  ? msg.payload.sessionId
553
570
  : undefined;
571
+ // Re-record mode: when the client (widget Specs tab or hover CLI)
572
+ // passes `reRecord: { slug }`, we collect tool_use events server-side
573
+ // into a SkillStep[] and, on session_end with no error, overwrite the
574
+ // existing __vibe_tests__/<slug>.spec.ts. This is the same flow the
575
+ // widget uses for "Save as Spec", but the spec already exists and is
576
+ // being regenerated for the current UI.
577
+ const reRecordSlug = msg.payload && typeof msg.payload === 'object' && 'reRecord' in msg.payload
578
+ ? msg.payload.reRecord?.slug
579
+ : undefined;
554
580
  if (typeof text !== 'string' || !text.trim())
555
581
  return;
556
582
  if (busy) {
@@ -563,6 +589,16 @@ export async function startService(opts) {
563
589
  busy = true;
564
590
  cancelled = false;
565
591
  inflight = new AbortController();
592
+ // Re-record step collector — populated as tool_use events stream by,
593
+ // consumed at session_end to overwrite the original spec. Empty unless
594
+ // reRecordSlug is set on this command. We seed with a synthetic
595
+ // `user` step so writeSpec's JSDoc Original-prompt: line carries the
596
+ // text the agent was actually given (which is the prompt we read out
597
+ // of the existing spec — the same one we're regenerating).
598
+ const reRecordSteps = [];
599
+ if (reRecordSlug) {
600
+ reRecordSteps.push({ kind: 'user', text });
601
+ }
566
602
  try {
567
603
  // Build the MCP config first — it's pure local file IO and lets
568
604
  // us assert plugin-contributed servers landed in the config even
@@ -684,6 +720,61 @@ export async function startService(opts) {
684
720
  if (cancelled || ws.readyState !== WebSocket.OPEN)
685
721
  return;
686
722
  send(ws, { type: 'event', payload: ev });
723
+ // Re-record collection. Mirror what widget client.js does on the
724
+ // way past tool_use events: accumulate into a SkillStep[] so we
725
+ // can write a fresh spec when the session ends. We do this only
726
+ // when this command was launched in re-record mode; ordinary
727
+ // commands don't need server-side step retention (widget owns
728
+ // that for normal saves).
729
+ if (reRecordSlug && ev.kind === 'tool_use') {
730
+ reRecordSteps.push({
731
+ kind: 'step',
732
+ tool: ev.tool,
733
+ input: ev.input,
734
+ });
735
+ }
736
+ if (reRecordSlug && ev.kind === 'session_end') {
737
+ // Cancelled or errored runs: don't overwrite — the existing
738
+ // spec is still valid. Tell the client what happened.
739
+ if (ev.isError) {
740
+ sendIfOpen(ws, {
741
+ type: 'error',
742
+ payload: {
743
+ message: `Re-record failed: ${ev.summary ?? 'agent reported an error'}. ` +
744
+ `Original spec left unchanged.`,
745
+ },
746
+ });
747
+ }
748
+ else {
749
+ // Snapshot the agent's final summary into a synthetic `done`
750
+ // step so writeSpec's `Outcome:` header reflects the new run.
751
+ if (ev.summary) {
752
+ reRecordSteps.push({ kind: 'done', summary: ev.summary });
753
+ }
754
+ // Overwrite. writeSpec uses the slug to name the file; we
755
+ // pass the original slug verbatim so the path is stable.
756
+ try {
757
+ const { writeSpec } = await import('./specs/writeSpec.js');
758
+ const result = await writeSpec({
759
+ devRoot,
760
+ name: reRecordSlug,
761
+ steps: reRecordSteps,
762
+ overwrite: true,
763
+ });
764
+ sendIfOpen(ws, {
765
+ type: 'spec-saved',
766
+ payload: { name: reRecordSlug, path: result.path },
767
+ });
768
+ }
769
+ catch (e) {
770
+ const m = e instanceof Error ? e.message : String(e);
771
+ sendIfOpen(ws, {
772
+ type: 'error',
773
+ payload: { message: `Re-record could not write spec: ${m}` },
774
+ });
775
+ }
776
+ }
777
+ }
687
778
  }
688
779
  }
689
780
  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.11.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",