@hover-dev/core 0.14.1 → 0.15.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 +73 -1
- package/dist/agents/claude.d.ts.map +1 -1
- package/dist/agents/claude.js +14 -0
- package/dist/agents/codex.d.ts.map +1 -1
- package/dist/agents/codex.js +1 -0
- package/dist/agents/invoke.d.ts.map +1 -1
- package/dist/agents/invoke.js +10 -1
- package/dist/agents/types.d.ts +11 -0
- package/dist/agents/types.d.ts.map +1 -1
- package/dist/playwright/resolveMcpConfig.d.ts +5 -0
- package/dist/playwright/resolveMcpConfig.d.ts.map +1 -1
- package/dist/playwright/resolveMcpConfig.js +2 -1
- package/dist/runSession.d.ts +42 -0
- package/dist/runSession.d.ts.map +1 -0
- package/dist/runSession.js +76 -0
- package/dist/service/cdpHint.d.ts.map +1 -1
- package/dist/service/cdpHint.js +30 -14
- package/dist/service/conventions.d.ts +8 -0
- package/dist/service/conventions.d.ts.map +1 -0
- package/dist/service/conventions.js +42 -0
- package/dist/service/saveHandlers.d.ts +10 -13
- package/dist/service/saveHandlers.d.ts.map +1 -1
- package/dist/service/saveHandlers.js +9 -25
- package/dist/service/types.d.ts +5 -0
- package/dist/service/types.d.ts.map +1 -1
- package/dist/service.d.ts +7 -4
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +141 -104
- package/dist/skills/writeSkill.d.ts +12 -35
- package/dist/skills/writeSkill.d.ts.map +1 -1
- package/dist/skills/writeSkill.js +10 -166
- package/dist/specs/detectSharedFlows.d.ts +35 -0
- package/dist/specs/detectSharedFlows.d.ts.map +1 -0
- package/dist/specs/detectSharedFlows.js +171 -0
- package/dist/specs/extractPageObjects.d.ts +18 -0
- package/dist/specs/extractPageObjects.d.ts.map +1 -0
- package/dist/specs/extractPageObjects.js +98 -0
- package/dist/specs/generatePageObject.d.ts +29 -0
- package/dist/specs/generatePageObject.d.ts.map +1 -0
- package/dist/specs/generatePageObject.js +149 -0
- package/dist/specs/listSpecs.d.ts +12 -0
- package/dist/specs/listSpecs.d.ts.map +1 -1
- package/dist/specs/listSpecs.js +27 -2
- package/dist/specs/optimizationSuggestion.d.ts +26 -0
- package/dist/specs/optimizationSuggestion.d.ts.map +1 -0
- package/dist/specs/optimizationSuggestion.js +28 -0
- package/dist/specs/optimizeSpec.d.ts +42 -0
- package/dist/specs/optimizeSpec.d.ts.map +1 -0
- package/dist/specs/optimizeSpec.js +166 -0
- package/dist/specs/optimizeSpecWithAgent.d.ts +11 -0
- package/dist/specs/optimizeSpecWithAgent.d.ts.map +1 -0
- package/dist/specs/optimizeSpecWithAgent.js +40 -0
- package/dist/specs/pageObjectManifest.d.ts +20 -0
- package/dist/specs/pageObjectManifest.d.ts.map +1 -0
- package/dist/specs/pageObjectManifest.js +40 -0
- package/dist/specs/seeds.d.ts +36 -0
- package/dist/specs/seeds.d.ts.map +1 -0
- package/dist/specs/seeds.js +74 -0
- package/dist/specs/sidecar.d.ts +25 -0
- package/dist/specs/sidecar.d.ts.map +1 -0
- package/dist/specs/sidecar.js +38 -0
- package/dist/specs/writeSpec.d.ts +50 -0
- package/dist/specs/writeSpec.d.ts.map +1 -1
- package/dist/specs/writeSpec.js +249 -75
- package/package.json +1 -2
package/README.md
CHANGED
|
@@ -26,6 +26,78 @@ The local Node service. Owns:
|
|
|
26
26
|
|
|
27
27
|
To add an agent: implement an `AgentDescriptor`, register it in `registry.ts`. Done.
|
|
28
28
|
|
|
29
|
+
## Spec generation (specs/)
|
|
30
|
+
|
|
31
|
+
A verified session crystallizes into a standard `@playwright/test` file under
|
|
32
|
+
`<devRoot>/__vibe_tests__/`. Translation is **deterministic — no LLM on the
|
|
33
|
+
per-save path** (reproducible by construction):
|
|
34
|
+
|
|
35
|
+
- `writeSpec.ts` walks the captured `browser_*` actions and emits one Playwright
|
|
36
|
+
call each (`getByRole` / `getByLabel` / `getByText` selectors — never XPath).
|
|
37
|
+
A few high-frequency multi-action shapes are hardcoded (popup / new-tab →
|
|
38
|
+
`Promise.all([context.waitForEvent('page'), …click()])`). An action with no
|
|
39
|
+
single-step translation (file upload, drag, …) leaves a structured
|
|
40
|
+
`// hover:optimizable: <tool>` marker rather than a `// TODO` — the draft stays
|
|
41
|
+
runnable around it. `countOptimizableMarkers()` reads the count back; `listSpecs`
|
|
42
|
+
surfaces it as `SpecSummary.optimizableCount`.
|
|
43
|
+
- Alongside the `.spec.ts`, a **sidecar** is written to
|
|
44
|
+
`.hover/<slug>.json` — the structured `SpecStep[]` + observed signals. This is
|
|
45
|
+
the machine-readable behavior record the optimization pass reads (it keeps the
|
|
46
|
+
spec itself clean).
|
|
47
|
+
|
|
48
|
+
### Optional optimization pass (F7)
|
|
49
|
+
|
|
50
|
+
The **service** (never the sandboxed browser agent) can optionally run an LLM
|
|
51
|
+
**codegen** call over a draft to polish it — chiefly to add assertions for the
|
|
52
|
+
feedback the session observed. Its input is data the service already holds (the
|
|
53
|
+
draft + sidecar + relevant seeds), not live page content, so it sits outside the
|
|
54
|
+
agent's prompt-injection surface and needs no filesystem access. It writes an
|
|
55
|
+
**optimization candidate** to `.hover/optimized/<slug>.spec.ts.draft` (never
|
|
56
|
+
`*.spec.ts`, so the test runner can't collect an unreviewed candidate). A human
|
|
57
|
+
promotes or discards it via diff — **the deterministic original is always
|
|
58
|
+
preserved**. Off by default.
|
|
59
|
+
|
|
60
|
+
### Seed library — extending translation (`.hover/rules/`)
|
|
61
|
+
|
|
62
|
+
The optimization pass generalizes from **seeds**: human-written worked examples
|
|
63
|
+
of "captured steps → the Playwright code they should produce." This is how
|
|
64
|
+
coverage of new multi-step patterns grows **without core changes** — you (or the
|
|
65
|
+
community) drop a JSON file; the pass picks it up as few-shot.
|
|
66
|
+
|
|
67
|
+
A seed lives at `<projectRoot>/.hover/rules/<name>.json` and matches
|
|
68
|
+
[`src/specs/seed.schema.json`](src/specs/seed.schema.json):
|
|
69
|
+
|
|
70
|
+
```json
|
|
71
|
+
{
|
|
72
|
+
"name": "oauth-popup",
|
|
73
|
+
"signature": ["browser_click", "browser_tabs:select"],
|
|
74
|
+
"note": "sign in through a provider popup that opens a new tab",
|
|
75
|
+
"example": {
|
|
76
|
+
"steps": [
|
|
77
|
+
{ "tool": "browser_click", "element": "Sign in with Google button" },
|
|
78
|
+
{ "tool": "browser_tabs", "action": "select", "idx": 1 }
|
|
79
|
+
],
|
|
80
|
+
"code": "const [popup] = await Promise.all([\n context.waitForEvent('page'),\n page.getByRole('button', { name: 'Sign in with Google' }).click(),\n]);\nawait popup.getByLabel('Email').fill('user@example.com');"
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
- **`signature`** is a cheap relevance filter only — `relevantSeeds()` keeps a
|
|
86
|
+
seed if any of its base tools (`browser_tabs:select` → `browser_tabs`) appears
|
|
87
|
+
in the spec being optimized. It is **not** exact-matched.
|
|
88
|
+
- **`code`** must obey the same rules as generated specs: semantic selectors, no
|
|
89
|
+
XPath, no `waitForTimeout`.
|
|
90
|
+
- **Built-in seeds** ship in `src/specs/seeds.ts` (`BUILTIN_SEEDS`). The bar to
|
|
91
|
+
be built-in is high — only **highly certain**, app-agnostic, deterministic
|
|
92
|
+
patterns qualify (currently just `download`). Semantic / judgement-based
|
|
93
|
+
optimizations (e.g. *which* feedback text to assert) are not seeds — they're
|
|
94
|
+
standing instructions in the prompt. Popup is hardcoded in `writeSpec.ts`, not
|
|
95
|
+
a seed. Speculative or project-specific patterns belong in your own
|
|
96
|
+
`.hover/rules/`, where the bar is your call.
|
|
97
|
+
|
|
98
|
+
`readSeeds(projectRoot)` returns built-ins + your `.hover/rules/*.json`
|
|
99
|
+
(malformed files are skipped, not fatal).
|
|
100
|
+
|
|
29
101
|
## Smoke test
|
|
30
102
|
|
|
31
103
|
```bash
|
|
@@ -56,7 +128,7 @@ Environment variables:
|
|
|
56
128
|
The `claude -p` invocation is locked down so Claude can only drive the browser:
|
|
57
129
|
|
|
58
130
|
- `--strict-mcp-config` — ignore any MCP servers in `~/.claude/` or `.mcp.json`
|
|
59
|
-
- `--allowedTools mcp__playwright
|
|
131
|
+
- `--allowedTools mcp__playwright` — only Playwright MCP is callable
|
|
60
132
|
- `--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`)
|
|
61
133
|
- `--permission-mode dontAsk` — anything not whitelisted aborts the run
|
|
62
134
|
- `--max-budget-usd <n>` — optional hard $ ceiling per session (no default; pass `maxBudgetUsd` in plugin options or via the CLI flag to enable)
|
|
@@ -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;
|
|
1
|
+
{"version":3,"file":"claude.d.ts","sourceRoot":"","sources":["../../src/agents/claude.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAA2C,MAAM,YAAY,CAAC;AA0H3F,eAAO,MAAM,WAAW,EAAE,eA2HzB,CAAC"}
|
package/dist/agents/claude.js
CHANGED
|
@@ -60,6 +60,19 @@ const CLAUDE_DEFAULT_DISALLOWED_TOOLS = [
|
|
|
60
60
|
'Monitor', 'TaskOutput', 'TaskStop',
|
|
61
61
|
'AskUserQuestion',
|
|
62
62
|
'ShareOnboardingGuide',
|
|
63
|
+
// Skills are loaded independently of the --allowedTools allow-list, so an
|
|
64
|
+
// allow-list of `mcp__playwright` does NOT block the `Skill` tool. Left
|
|
65
|
+
// through, the agent burns a turn "checking for a project skill first" and
|
|
66
|
+
// pollutes the crystallized spec with a junk `When · Skill` step. Deny it.
|
|
67
|
+
'Skill',
|
|
68
|
+
// Playwright MCP's arbitrary-JS tools. browser_run_code_unsafe /
|
|
69
|
+
// browser_evaluate run any JS in the page — a real prompt-injection exfil
|
|
70
|
+
// path (fetch a token out, read localStorage) that punches through the
|
|
71
|
+
// "Playwright MCP only" sandbox, and their output can't be translated into
|
|
72
|
+
// a deterministic Playwright spec anyway (it lands as a `// TODO`). Agents
|
|
73
|
+
// drive via click/fill/select and read state via snapshot instead.
|
|
74
|
+
'mcp__playwright__browser_run_code_unsafe',
|
|
75
|
+
'mcp__playwright__browser_evaluate',
|
|
63
76
|
];
|
|
64
77
|
export const claudeAgent = {
|
|
65
78
|
id: 'claude',
|
|
@@ -68,6 +81,7 @@ export const claudeAgent = {
|
|
|
68
81
|
streamFormat: 'stream-json',
|
|
69
82
|
sandboxStrength: 'hard',
|
|
70
83
|
defaultDisallowedTools: CLAUDE_DEFAULT_DISALLOWED_TOOLS,
|
|
84
|
+
apiKeyEnv: 'ANTHROPIC_API_KEY',
|
|
71
85
|
display: {
|
|
72
86
|
label: 'Claude Code',
|
|
73
87
|
tagline: 'Anthropic — best-in-class browser driving, hard tool sandbox',
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"codex.d.ts","sourceRoot":"","sources":["../../src/agents/codex.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAA8B,WAAW,EAAE,MAAM,YAAY,CAAC;AAmK3F,eAAO,MAAM,UAAU,EAAE,
|
|
1
|
+
{"version":3,"file":"codex.d.ts","sourceRoot":"","sources":["../../src/agents/codex.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAA8B,WAAW,EAAE,MAAM,YAAY,CAAC;AAmK3F,eAAO,MAAM,UAAU,EAAE,eA0JxB,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,SAAS;sBACJ,WAAW;2BACJ,WAAW;sBAChB,WAAW;;;;;;;CAU9B,CAAC"}
|
package/dist/agents/codex.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"invoke.d.ts","sourceRoot":"","sources":["../../src/agents/invoke.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAe,MAAM,YAAY,CAAC;AAE1E;;;;;;;;GAQG;AACH,wBAAuB,WAAW,CAAC,IAAI,EAAE,aAAa,GAAG,aAAa,CAAC,WAAW,CAAC,
|
|
1
|
+
{"version":3,"file":"invoke.d.ts","sourceRoot":"","sources":["../../src/agents/invoke.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAe,MAAM,YAAY,CAAC;AAE1E;;;;;;;;GAQG;AACH,wBAAuB,WAAW,CAAC,IAAI,EAAE,aAAa,GAAG,aAAa,CAAC,WAAW,CAAC,CA+ElF"}
|
package/dist/agents/invoke.js
CHANGED
|
@@ -31,7 +31,16 @@ export async function* invokeAgent(opts) {
|
|
|
31
31
|
cwd: opts.cwd,
|
|
32
32
|
// Clear CLAUDECODE so spawning `claude` from inside a Claude Code session
|
|
33
33
|
// doesn't trip the nested-session guard. Harmless for other agents.
|
|
34
|
-
|
|
34
|
+
// If the caller supplied an API key and the descriptor names a key env var,
|
|
35
|
+
// inject it so the CLI runs on the key instead of a logged-in subscription.
|
|
36
|
+
// The key lives only in this child's env — never logged, never persisted.
|
|
37
|
+
env: {
|
|
38
|
+
...process.env,
|
|
39
|
+
CLAUDECODE: '',
|
|
40
|
+
...(opts.apiKey && descriptor.apiKeyEnv
|
|
41
|
+
? { [descriptor.apiKeyEnv]: opts.apiKey }
|
|
42
|
+
: {}),
|
|
43
|
+
},
|
|
35
44
|
});
|
|
36
45
|
const onAbort = () => {
|
|
37
46
|
if (!child.killed)
|
package/dist/agents/types.d.ts
CHANGED
|
@@ -31,6 +31,11 @@ export interface InvokeOptions {
|
|
|
31
31
|
* "the user's current Chrome tab is already on http://localhost:5173/,
|
|
32
32
|
* don't browser_navigate there". */
|
|
33
33
|
appendSystemPrompt?: string;
|
|
34
|
+
/** Optional model API key. Injected into the spawned CLI's environment under
|
|
35
|
+
* the descriptor's `apiKeyEnv` var (e.g. ANTHROPIC_API_KEY) so a user without
|
|
36
|
+
* a logged-in subscription can drive Hover with their own key. Never logged,
|
|
37
|
+
* never persisted server-side — held only for the lifetime of the spawn. */
|
|
38
|
+
apiKey?: string;
|
|
34
39
|
/** Aborts the spawned child if signaled. Used to stop an orphan run when
|
|
35
40
|
* the WebSocket caller disconnects (e.g. user reloads the dev page). */
|
|
36
41
|
signal?: AbortSignal;
|
|
@@ -151,6 +156,12 @@ export interface AgentDescriptor {
|
|
|
151
156
|
* per-CLI deny list live alongside its descriptor instead of as a magic
|
|
152
157
|
* array in the service. Soft-sandbox agents leave this undefined. */
|
|
153
158
|
defaultDisallowedTools?: readonly string[];
|
|
159
|
+
/** Environment variable this CLI reads its model API key from
|
|
160
|
+
* (claude: ANTHROPIC_API_KEY, codex: OPENAI_API_KEY). When set and the
|
|
161
|
+
* caller supplies `InvokeOptions.apiKey`, the key is injected into the spawn
|
|
162
|
+
* env so the user can run on a raw key instead of a logged-in subscription.
|
|
163
|
+
* Undefined for agents that have no API-key env path. */
|
|
164
|
+
apiKeyEnv?: string;
|
|
154
165
|
buildArgs(opts: InvokeOptions): string[];
|
|
155
166
|
/**
|
|
156
167
|
* 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":"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"}
|
|
@@ -40,5 +40,10 @@ export declare function resolveMcpConfig(opts: {
|
|
|
40
40
|
/** Suffix for the output filename so multiple parallel configs from
|
|
41
41
|
* the same service (e.g. mode toggle round-trips) don't share state. */
|
|
42
42
|
suffix?: string;
|
|
43
|
+
/** Project root to resolve `@playwright/mcp` from. Defaults to
|
|
44
|
+
* `process.cwd()`. `hover run --cwd apps/web` passes the target workspace
|
|
45
|
+
* so a monorepo that installed `@hover-dev/core` only under that app (not
|
|
46
|
+
* the repo root the CLI was invoked from) still resolves the MCP package. */
|
|
47
|
+
cwd?: string;
|
|
43
48
|
}): string;
|
|
44
49
|
//# sourceMappingURL=resolveMcpConfig.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resolveMcpConfig.d.ts","sourceRoot":"","sources":["../../src/playwright/resolveMcpConfig.ts"],"names":[],"mappings":"AAMA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,WAAW,cAAc;IAC7B;2EACuE;IACvE,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC9B;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE;IACrC,gEAAgE;IAChE,MAAM,EAAE,MAAM,CAAC;IACf,6DAA6D;IAC7D,IAAI,EAAE,MAAM,CAAC;IACb;;;;2CAIuC;IACvC,KAAK,CAAC,EAAE,cAAc,EAAE,CAAC;IACzB;6EACyE;IACzE,MAAM,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"resolveMcpConfig.d.ts","sourceRoot":"","sources":["../../src/playwright/resolveMcpConfig.ts"],"names":[],"mappings":"AAMA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,WAAW,cAAc;IAC7B;2EACuE;IACvE,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC9B;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE;IACrC,gEAAgE;IAChE,MAAM,EAAE,MAAM,CAAC;IACf,6DAA6D;IAC7D,IAAI,EAAE,MAAM,CAAC;IACb;;;;2CAIuC;IACvC,KAAK,CAAC,EAAE,cAAc,EAAE,CAAC;IACzB;6EACyE;IACzE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;kFAG8E;IAC9E,GAAG,CAAC,EAAE,MAAM,CAAC;CACd,GAAG,MAAM,CAiDT"}
|
|
@@ -19,7 +19,8 @@ export function resolveMcpConfig(opts) {
|
|
|
19
19
|
// can't actually load. `process.cwd()` is the user's project root,
|
|
20
20
|
// and `@playwright/mcp` is always reachable from there because it's
|
|
21
21
|
// a declared dependency of `@hover-dev/core`, which the user installed.
|
|
22
|
-
|
|
22
|
+
// The caller may override with an explicit `cwd` (e.g. `hover run --cwd`).
|
|
23
|
+
const require = createRequire(resolve(opts.cwd ?? process.cwd(), 'package.json'));
|
|
23
24
|
const pkgJsonPath = require.resolve('@playwright/mcp/package.json');
|
|
24
25
|
const pkgRoot = dirname(pkgJsonPath);
|
|
25
26
|
// The package's `bin` map declares "playwright-mcp": "cli.js" — we
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { InvokeEvent } from './agents/types.js';
|
|
2
|
+
import type { SkillStep } from './skills/writeSkill.js';
|
|
3
|
+
export interface RunSessionOptions {
|
|
4
|
+
prompt: string;
|
|
5
|
+
agentId: string;
|
|
6
|
+
/** CDP URL of the debug Chrome the agent drives. Required unless `mcpConfig`
|
|
7
|
+
* is supplied (the service passes a pre-built config; the CLI passes this). */
|
|
8
|
+
cdpUrl?: string;
|
|
9
|
+
model?: string;
|
|
10
|
+
maxBudgetUsd?: number;
|
|
11
|
+
/** Optional model API key, injected into the spawned CLI's env. */
|
|
12
|
+
apiKey?: string;
|
|
13
|
+
/** Agent cwd (project root) — where Claude Code reads CLAUDE.md and where a
|
|
14
|
+
* `--save` / re-record writes the spec. Defaults to the process cwd. */
|
|
15
|
+
cwd?: string;
|
|
16
|
+
/** Namespaces the temp MCP config filename. Defaults to 51789. */
|
|
17
|
+
port?: number;
|
|
18
|
+
signal?: AbortSignal;
|
|
19
|
+
/** Pre-built MCP config path. The service supplies one (with plugin servers);
|
|
20
|
+
* when omitted, runSession builds a plugin-free Playwright config from
|
|
21
|
+
* `cdpUrl` via resolveMcpConfig. */
|
|
22
|
+
mcpConfig?: string;
|
|
23
|
+
/** Extra hard-sandbox allow-list prefixes — e.g. active-mode plugin MCP
|
|
24
|
+
* server ids the service contributes. Appended to ['mcp__playwright']. */
|
|
25
|
+
allowedToolsExtra?: string[];
|
|
26
|
+
/** Appended to the agent's system prompt (the service folds in cdpHint +
|
|
27
|
+
* conventions + plugin additions + a language directive; the CLI omits it). */
|
|
28
|
+
appendSystemPrompt?: string;
|
|
29
|
+
/** Resume an existing agent session (a follow-up turn). */
|
|
30
|
+
sessionId?: string;
|
|
31
|
+
}
|
|
32
|
+
export interface RunSessionResult {
|
|
33
|
+
/** Captured session as SpecStep[] (`user` → `step`* → `done`), ready to hand
|
|
34
|
+
* straight to `writeSpec`. */
|
|
35
|
+
steps: SkillStep[];
|
|
36
|
+
/** The agent's final summary, if any. */
|
|
37
|
+
summary: string;
|
|
38
|
+
/** True if the run ended in error or was aborted. */
|
|
39
|
+
isError: boolean;
|
|
40
|
+
}
|
|
41
|
+
export declare function runSession(opts: RunSessionOptions, onEvent: (ev: InvokeEvent) => void): Promise<RunSessionResult>;
|
|
42
|
+
//# sourceMappingURL=runSession.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runSession.d.ts","sourceRoot":"","sources":["../src/runSession.ts"],"names":[],"mappings":"AAoBA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAGxD,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB;oFACgF;IAChF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,mEAAmE;IACnE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;6EACyE;IACzE,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,kEAAkE;IAClE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB;;yCAEqC;IACrC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;+EAC2E;IAC3E,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC7B;oFACgF;IAChF,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,2DAA2D;IAC3D,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,gBAAgB;IAC/B;mCAC+B;IAC/B,KAAK,EAAE,SAAS,EAAE,CAAC;IACnB,yCAAyC;IACzC,OAAO,EAAE,MAAM,CAAC;IAChB,qDAAqD;IACrD,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,wBAAsB,UAAU,CAC9B,IAAI,EAAE,iBAAiB,EACvB,OAAO,EAAE,CAAC,EAAE,EAAE,WAAW,KAAK,IAAI,GACjC,OAAO,CAAC,gBAAgB,CAAC,CAuD3B"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Headless session runner — the invoke + crystallize engine shared by every
|
|
3
|
+
* frontend. The widget reaches it through the WebSocket service; `smoke.ts`
|
|
4
|
+
* and (future) `hover run` call it in-process, no WS server. It spawns the
|
|
5
|
+
* agent against the user's debug Chrome over CDP, streams normalized events to
|
|
6
|
+
* `onEvent`, and accumulates the captured tool calls into a `SpecStep[]` the
|
|
7
|
+
* caller can hand to `writeSpec` — `user` seed → `step` per tool_use → `done`
|
|
8
|
+
* with the final summary (the exact shape the spec pipeline consumes).
|
|
9
|
+
*
|
|
10
|
+
* No WebSocket, no DOM. It drives an *already-running* debug Chrome over CDP;
|
|
11
|
+
* launching Chrome / CDP preflight is the caller's call (the service does it
|
|
12
|
+
* with autoLaunch; the CLI will too). The sandbox (allow/deny tools) mirrors
|
|
13
|
+
* the service exactly, gated on the agent's `sandboxStrength`.
|
|
14
|
+
*
|
|
15
|
+
* The full surface (mcpConfig override, allowedToolsExtra, appendSystemPrompt,
|
|
16
|
+
* sessionId) lets the service delegate to this instead of duplicating the
|
|
17
|
+
* invoke loop; the CLI uses only the small subset (prompt + cdpUrl + model).
|
|
18
|
+
*/
|
|
19
|
+
import { invokeAgent } from './agents/invoke.js';
|
|
20
|
+
import { getAgent } from './agents/registry.js';
|
|
21
|
+
import { resolveMcpConfig } from './playwright/resolveMcpConfig.js';
|
|
22
|
+
export async function runSession(opts, onEvent) {
|
|
23
|
+
const descriptor = getAgent(opts.agentId);
|
|
24
|
+
const isHardSandbox = descriptor?.sandboxStrength === 'hard';
|
|
25
|
+
// Seed with a synthetic `user` step so writeSpec's JSDoc `Original prompt:`
|
|
26
|
+
// line carries the prompt the agent was given (mirrors the service path).
|
|
27
|
+
const steps = [{ kind: 'user', text: opts.prompt }];
|
|
28
|
+
let summary = '';
|
|
29
|
+
let isError = false;
|
|
30
|
+
const mcpConfig = opts.mcpConfig ??
|
|
31
|
+
resolveMcpConfig({
|
|
32
|
+
cdpUrl: opts.cdpUrl ?? 'http://localhost:9222',
|
|
33
|
+
port: opts.port ?? 51789,
|
|
34
|
+
// Resolve @playwright/mcp from the run's cwd, not the dir the CLI was
|
|
35
|
+
// invoked from — `hover run --cwd apps/web` must find the MCP package
|
|
36
|
+
// under the target workspace in a monorepo.
|
|
37
|
+
cwd: opts.cwd,
|
|
38
|
+
});
|
|
39
|
+
for await (const ev of invokeAgent({
|
|
40
|
+
agentId: opts.agentId,
|
|
41
|
+
prompt: opts.prompt,
|
|
42
|
+
sessionId: opts.sessionId,
|
|
43
|
+
mcpConfig,
|
|
44
|
+
cwd: opts.cwd,
|
|
45
|
+
appendSystemPrompt: opts.appendSystemPrompt,
|
|
46
|
+
// Hard sandbox: only Playwright MCP (+ any active-mode plugin servers) is
|
|
47
|
+
// callable, every built-in tool denied — a hijacked prompt can't reach the
|
|
48
|
+
// shell or filesystem. Soft agents (codex, …) enforce their own sandbox via
|
|
49
|
+
// buildArgs, so the lists stay undefined for them — exactly what the
|
|
50
|
+
// service does.
|
|
51
|
+
allowedTools: isHardSandbox
|
|
52
|
+
? ['mcp__playwright', ...(opts.allowedToolsExtra ?? [])]
|
|
53
|
+
: undefined,
|
|
54
|
+
disallowedTools: isHardSandbox
|
|
55
|
+
? (descriptor?.defaultDisallowedTools ? [...descriptor.defaultDisallowedTools] : undefined)
|
|
56
|
+
: undefined,
|
|
57
|
+
maxBudgetUsd: opts.maxBudgetUsd,
|
|
58
|
+
model: opts.model,
|
|
59
|
+
apiKey: opts.apiKey,
|
|
60
|
+
signal: opts.signal,
|
|
61
|
+
})) {
|
|
62
|
+
onEvent(ev);
|
|
63
|
+
if (ev.kind === 'tool_use') {
|
|
64
|
+
steps.push({ kind: 'step', tool: ev.tool, input: ev.input });
|
|
65
|
+
}
|
|
66
|
+
else if (ev.kind === 'session_end') {
|
|
67
|
+
if (ev.summary)
|
|
68
|
+
summary = ev.summary;
|
|
69
|
+
if (ev.isError)
|
|
70
|
+
isError = true;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (summary)
|
|
74
|
+
steps.push({ kind: 'done', summary });
|
|
75
|
+
return { steps, summary, isError };
|
|
76
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cdpHint.d.ts","sourceRoot":"","sources":["../../src/service/cdpHint.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,UAAU,GAAG;IAAG,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE;AAa7C,wBAAgB,YAAY,CAAC,IAAI,EAAE,GAAG,EAAE,GAAG,MAAM,
|
|
1
|
+
{"version":3,"file":"cdpHint.d.ts","sourceRoot":"","sources":["../../src/service/cdpHint.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,UAAU,GAAG;IAAG,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE;AAa7C,wBAAgB,YAAY,CAAC,IAAI,EAAE,GAAG,EAAE,GAAG,MAAM,CAmJhD;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,GAAG,EAAE,GAAG,MAAM,CAYtD"}
|
package/dist/service/cdpHint.js
CHANGED
|
@@ -45,17 +45,24 @@ export function buildCdpHint(tabs) {
|
|
|
45
45
|
return [
|
|
46
46
|
`Your job — read this first:`,
|
|
47
47
|
``,
|
|
48
|
-
` You are an end-to-end testing agent.
|
|
49
|
-
` the user's
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
`
|
|
53
|
-
`
|
|
54
|
-
|
|
55
|
-
`
|
|
56
|
-
`
|
|
57
|
-
`
|
|
58
|
-
`
|
|
48
|
+
` You are an end-to-end testing agent. Match the scope of your run to how`,
|
|
49
|
+
` specific the user's prompt is — do NOT over-test.`,
|
|
50
|
+
``,
|
|
51
|
+
` SPECIFIC prompt — it names a flow or action ("log in as alice and add a`,
|
|
52
|
+
` todo", "test the login flow", "只测试登录"): do EXACTLY that flow and`,
|
|
53
|
+
` verify its outcome, then STOP. Stay inside the named scope. Do NOT wander`,
|
|
54
|
+
` into adjacent flows, extra edge cases (empty/invalid input, boundary`,
|
|
55
|
+
` values), logout, or bug-hunting unless the prompt explicitly asks. A`,
|
|
56
|
+
` focused run that does what was asked and asserts the result is the goal,`,
|
|
57
|
+
` not breadth — one clean verified flow is a complete, successful result.`,
|
|
58
|
+
` But if you DO hit a real problem while doing the asked flow — a broken`,
|
|
59
|
+
` button, a wrong message, a console error, a failed verification — still`,
|
|
60
|
+
` report it under ## Findings. Don't go hunting for more; just don't swallow`,
|
|
61
|
+
` what you ran into.`,
|
|
62
|
+
``,
|
|
63
|
+
` VAGUE or short prompt ("test", "check", "see if it works", "find bugs",`,
|
|
64
|
+
` or a single word): DO NOT ask for clarification and DO NOT just take a`,
|
|
65
|
+
` snapshot and call it done. Run a real exploratory test pass:`,
|
|
59
66
|
``,
|
|
60
67
|
` 1. browser_snapshot to learn the app's structure.`,
|
|
61
68
|
` 2. Identify the main interactive surfaces (forms, buttons, links,`,
|
|
@@ -68,9 +75,8 @@ export function buildCdpHint(tabs) {
|
|
|
68
75
|
` confusing in the final summary's "## Findings" section.`,
|
|
69
76
|
``,
|
|
70
77
|
` A short "App is running fine" reply after one snapshot is NOT an`,
|
|
71
|
-
` acceptable result
|
|
72
|
-
` flows to confirm it, or you found something interesting
|
|
73
|
-
` the only two valid outcomes of a vague prompt.`,
|
|
78
|
+
` acceptable result for a vague prompt — either the app works and you ran`,
|
|
79
|
+
` several flows to confirm it, or you found something interesting.`,
|
|
74
80
|
``,
|
|
75
81
|
`The user's Chrome currently has these tabs open:`,
|
|
76
82
|
...tabs.map(t => ` - ${t.url}${t.title ? ` (${t.title})` : ''}`),
|
|
@@ -140,6 +146,16 @@ export function buildCdpHint(tabs) {
|
|
|
140
146
|
` postMessage listener or auth refresh"); do NOT browser_navigate to`,
|
|
141
147
|
` same-origin to force a refresh (rule #2 still applies).`,
|
|
142
148
|
``,
|
|
149
|
+
`Tool usage — operate and verify through the structured Playwright tools:`,
|
|
150
|
+
``,
|
|
151
|
+
` 8. Drive the page only with click / fill / select / snapshot / wait. Do`,
|
|
152
|
+
` NOT use browser_run_code_unsafe or browser_evaluate to run JavaScript`,
|
|
153
|
+
` — they are disabled, and any action taken in raw JS cannot be`,
|
|
154
|
+
` crystallized into a deterministic Playwright spec (it is dropped as a`,
|
|
155
|
+
` TODO). To VERIFY an outcome, assert on what browser_snapshot shows —`,
|
|
156
|
+
` a heading, an error message, a counter value; the accessibility tree`,
|
|
157
|
+
` already exposes the text and roles you need.`,
|
|
158
|
+
``,
|
|
143
159
|
`Narration format — affects how the widget renders your run for the user:`,
|
|
144
160
|
``,
|
|
145
161
|
` Before each LOGICAL STEP (a coherent unit of work like "Open the login`,
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Max characters of the conventions file folded into the prompt. */
|
|
2
|
+
export declare const CONVENTIONS_MAX_CHARS = 4000;
|
|
3
|
+
/**
|
|
4
|
+
* Read `<projectRoot>/.hover/conventions.md` and return it wrapped as a
|
|
5
|
+
* system-prompt block, or null when the file is absent or empty.
|
|
6
|
+
*/
|
|
7
|
+
export declare function readConventions(projectRoot: string, maxChars?: number): Promise<string | null>;
|
|
8
|
+
//# sourceMappingURL=conventions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"conventions.d.ts","sourceRoot":"","sources":["../../src/service/conventions.ts"],"names":[],"mappings":"AAgBA,qEAAqE;AACrE,eAAO,MAAM,qBAAqB,OAAO,CAAC;AAE1C;;;GAGG;AACH,wBAAsB,eAAe,CACnC,WAAW,EAAE,MAAM,EACnB,QAAQ,SAAwB,GAC/B,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAqBxB"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Knowledge layer (F5): the project's testing conventions, injected into the
|
|
3
|
+
* agent's system prompt so the developer can steer *how it explores* — which
|
|
4
|
+
* flows matter, where login lives, the preferred selector attribute.
|
|
5
|
+
*
|
|
6
|
+
* Read by the SERVICE (not the agent) from `<projectRoot>/.hover/conventions.md`
|
|
7
|
+
* and folded into the system prompt — the agent never gains a file-read tool
|
|
8
|
+
* (D2). This shapes exploration only; it does NOT change how the saved spec is
|
|
9
|
+
* generated (that's the translator's job — D9).
|
|
10
|
+
*
|
|
11
|
+
* Capped to avoid prompt bloat, and injected on the FIRST turn only (it's
|
|
12
|
+
* static, like cdpHint's rules) so it doesn't fragment the prompt cache.
|
|
13
|
+
*/
|
|
14
|
+
import { readFile } from 'node:fs/promises';
|
|
15
|
+
import { join } from 'node:path';
|
|
16
|
+
/** Max characters of the conventions file folded into the prompt. */
|
|
17
|
+
export const CONVENTIONS_MAX_CHARS = 4000;
|
|
18
|
+
/**
|
|
19
|
+
* Read `<projectRoot>/.hover/conventions.md` and return it wrapped as a
|
|
20
|
+
* system-prompt block, or null when the file is absent or empty.
|
|
21
|
+
*/
|
|
22
|
+
export async function readConventions(projectRoot, maxChars = CONVENTIONS_MAX_CHARS) {
|
|
23
|
+
let raw;
|
|
24
|
+
try {
|
|
25
|
+
raw = await readFile(join(projectRoot, '.hover', 'conventions.md'), 'utf-8');
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return null; // no conventions file — nothing to inject
|
|
29
|
+
}
|
|
30
|
+
const trimmed = raw.trim();
|
|
31
|
+
if (!trimmed)
|
|
32
|
+
return null;
|
|
33
|
+
const body = trimmed.length > maxChars ? `${trimmed.slice(0, maxChars)}\n…(truncated)` : trimmed;
|
|
34
|
+
return [
|
|
35
|
+
`Project testing conventions — the developer's house rules for this app,`,
|
|
36
|
+
`from .hover/conventions.md. Use them while EXPLORING (which flows matter,`,
|
|
37
|
+
`where login lives, preferred selectors, test data). They guide exploration`,
|
|
38
|
+
`only — they do not change how the saved spec is generated.`,
|
|
39
|
+
``,
|
|
40
|
+
body,
|
|
41
|
+
].join('\n');
|
|
42
|
+
}
|
|
@@ -1,17 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Save-artifact WebSocket handlers (
|
|
2
|
+
* Save-artifact WebSocket handlers (spec / Jira CSV).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* `
|
|
9
|
-
*
|
|
10
|
-
* Replaces three near-identical handlers that drifted apart over time —
|
|
11
|
-
* see the v0.2.x refactor pass for the full rationale.
|
|
4
|
+
* Both save-* messages share the same shape: validate `name + steps`, call a
|
|
5
|
+
* per-kind writer, fork on Exists-error vs. success. The differences (which
|
|
6
|
+
* writer, which message names, which fields to pluck) are captured in the
|
|
7
|
+
* `SaveArtifactConfig` descriptor table below. (Save-as-Skill was retired; the
|
|
8
|
+
* generic `onSaved` hook it used is kept for any future artifact that needs a
|
|
9
|
+
* post-write tail.)
|
|
12
10
|
*/
|
|
13
11
|
import type { WebSocket } from 'ws';
|
|
14
|
-
import {
|
|
12
|
+
import { type SkillStep } from '../skills/writeSkill.js';
|
|
15
13
|
import { writeSpec, type SpecAssertion } from '../specs/writeSpec.js';
|
|
16
14
|
import { writeCaseCsv } from '../specs/writeCaseCsv.js';
|
|
17
15
|
import { type ClientMessage } from './types.js';
|
|
@@ -25,8 +23,8 @@ interface SaveArtifactConfig<TWriteResult extends {
|
|
|
25
23
|
savedType: string;
|
|
26
24
|
/** Emitted when the writer threw an Exists-error. */
|
|
27
25
|
existsType: string;
|
|
28
|
-
/** Optional — fires after a successful write.
|
|
29
|
-
*
|
|
26
|
+
/** Optional — fires after a successful write (e.g. to push a refreshed list
|
|
27
|
+
* to the widget). Currently unused; kept generic for future artifacts. */
|
|
30
28
|
onSaved?: (ws: WebSocket, devRoot: string) => Promise<void>;
|
|
31
29
|
/** Class used in `err instanceof …` to detect "already exists" errors. */
|
|
32
30
|
ExistsError: new (...args: never[]) => {
|
|
@@ -48,7 +46,6 @@ export declare function handleSaveArtifact<TWriteResult extends {
|
|
|
48
46
|
slug: string;
|
|
49
47
|
path: string;
|
|
50
48
|
}>(ws: WebSocket, msg: ClientMessage, devRoot: string, cfg: SaveArtifactConfig<TWriteResult>): Promise<void>;
|
|
51
|
-
export declare const SKILL_CONFIG: SaveArtifactConfig<Awaited<ReturnType<typeof writeSkill>>>;
|
|
52
49
|
export declare const SPEC_CONFIG: SaveArtifactConfig<Awaited<ReturnType<typeof writeSpec>>>;
|
|
53
50
|
export declare const CASE_CSV_CONFIG: SaveArtifactConfig<Awaited<ReturnType<typeof writeCaseCsv>>>;
|
|
54
51
|
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"saveHandlers.d.ts","sourceRoot":"","sources":["../../src/service/saveHandlers.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"saveHandlers.d.ts","sourceRoot":"","sources":["../../src/service/saveHandlers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,IAAI,CAAC;AACpC,OAAO,EAAE,KAAK,SAAS,EAAE,MAAM,yBAAyB,CAAC;AACzD,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;+EAC2E;IAC3E,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,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,16 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Save-artifact WebSocket handlers (
|
|
2
|
+
* Save-artifact WebSocket handlers (spec / Jira CSV).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* `
|
|
9
|
-
*
|
|
10
|
-
* Replaces three near-identical handlers that drifted apart over time —
|
|
11
|
-
* see the v0.2.x refactor pass for the full rationale.
|
|
4
|
+
* Both save-* messages share the same shape: validate `name + steps`, call a
|
|
5
|
+
* per-kind writer, fork on Exists-error vs. success. The differences (which
|
|
6
|
+
* writer, which message names, which fields to pluck) are captured in the
|
|
7
|
+
* `SaveArtifactConfig` descriptor table below. (Save-as-Skill was retired; the
|
|
8
|
+
* generic `onSaved` hook it used is kept for any future artifact that needs a
|
|
9
|
+
* post-write tail.)
|
|
12
10
|
*/
|
|
13
|
-
import { writeSkill, listSkills, SkillExistsError } from '../skills/writeSkill.js';
|
|
14
11
|
import { writeSpec, SpecExistsError } from '../specs/writeSpec.js';
|
|
15
12
|
import { writeCaseCsv, CaseCsvExistsError } from '../specs/writeCaseCsv.js';
|
|
16
13
|
import { send } from './types.js';
|
|
@@ -45,8 +42,8 @@ export async function handleSaveArtifact(ws, msg, devRoot, cfg) {
|
|
|
45
42
|
return;
|
|
46
43
|
}
|
|
47
44
|
send(ws, { type: cfg.savedType, payload: { name: result.slug, path: result.path } });
|
|
48
|
-
// The artifact is already on disk; an onSaved failure (e.g.
|
|
49
|
-
// re-scan) shouldn't surface as if the save itself failed — log
|
|
45
|
+
// The artifact is already on disk; an onSaved failure (e.g. a follow-up
|
|
46
|
+
// list re-scan) shouldn't surface as if the save itself failed — log on.
|
|
50
47
|
if (cfg.onSaved) {
|
|
51
48
|
try {
|
|
52
49
|
await cfg.onSaved(ws, devRoot);
|
|
@@ -57,19 +54,6 @@ export async function handleSaveArtifact(ws, msg, devRoot, cfg) {
|
|
|
57
54
|
}
|
|
58
55
|
}
|
|
59
56
|
}
|
|
60
|
-
export const SKILL_CONFIG = {
|
|
61
|
-
requestName: 'save-skill',
|
|
62
|
-
savedType: 'skill-saved',
|
|
63
|
-
existsType: 'skill-exists',
|
|
64
|
-
ExistsError: SkillExistsError,
|
|
65
|
-
write: ({ devRoot, name, description, steps, overwrite }) => writeSkill({ devRoot, name, description, steps, overwrite }),
|
|
66
|
-
onSaved: async (ws, devRoot) => {
|
|
67
|
-
// Push a fresh list so the widget's skills overlay updates without a
|
|
68
|
-
// round-trip — most relevant right after the save.
|
|
69
|
-
const skills = await listSkills(devRoot);
|
|
70
|
-
send(ws, { type: 'skills-list', payload: { skills } });
|
|
71
|
-
},
|
|
72
|
-
};
|
|
73
57
|
export const SPEC_CONFIG = {
|
|
74
58
|
requestName: 'save-spec',
|
|
75
59
|
savedType: 'spec-saved',
|
package/dist/service/types.d.ts
CHANGED
|
@@ -35,6 +35,11 @@ export interface ClientMessage {
|
|
|
35
35
|
/** set-mode only — id of the plugin-contributed mode to activate,
|
|
36
36
|
* or null to return to normal (unmoded) operation. */
|
|
37
37
|
modeId?: string | null;
|
|
38
|
+
/** optimize-spec / promote-optimized / discard-optimized — the spec slug. */
|
|
39
|
+
slug?: string;
|
|
40
|
+
/** set-api-key only — the model API key to inject into the spawned CLI's
|
|
41
|
+
* env (or empty/missing to clear it). Held in memory only, never logged. */
|
|
42
|
+
key?: string;
|
|
38
43
|
};
|
|
39
44
|
}
|
|
40
45
|
export declare function send(ws: WebSocket, message: {
|
|
@@ -1 +1 @@
|
|
|
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;
|
|
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;QACvB,6EAA6E;QAC7E,IAAI,CAAC,EAAE,MAAM,CAAC;QACd;qFAC6E;QAC7E,GAAG,CAAC,EAAE,MAAM,CAAC;KACd,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"}
|