@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/dist/service.d.ts
CHANGED
|
@@ -4,13 +4,16 @@ export interface ServiceOptions {
|
|
|
4
4
|
agentId?: string;
|
|
5
5
|
model?: string;
|
|
6
6
|
maxBudgetUsd?: number;
|
|
7
|
+
/** How the optimization pass (F7) surfaces in the widget. Default 'suggest'.
|
|
8
|
+
* 'off' = no nudge, 'suggest' = ✦ hint, 'on' = auto-run after Save-as-spec. */
|
|
9
|
+
optimizeMode?: 'off' | 'suggest' | 'on';
|
|
7
10
|
mcpConfig?: string;
|
|
8
11
|
/** CDP URL to preflight before each command (default http://localhost:9222). */
|
|
9
12
|
cdpUrl?: string;
|
|
10
|
-
/** Working directory for the spawned agent. Also
|
|
11
|
-
* (
|
|
12
|
-
*
|
|
13
|
-
*
|
|
13
|
+
/** Working directory for the spawned agent. Also the root under which saved
|
|
14
|
+
* specs (`__vibe_tests__/`), sidecars + seeds (`.hover/`) live. Defaults to
|
|
15
|
+
* process.cwd(); in Vite plugin context, set to `server.config.root` so the
|
|
16
|
+
* agent runs against the project (and Claude Code reads its CLAUDE.md). */
|
|
14
17
|
devRoot?: string;
|
|
15
18
|
/** Plugins contributed by the bundler-plugin wrapper. Each manifest can
|
|
16
19
|
* add a widget mode, MCP servers, Chrome flags, and lifecycle hooks.
|
package/dist/service.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"
|
|
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;oFACgF;IAChF,YAAY,CAAC,EAAE,KAAK,GAAG,SAAS,GAAG,IAAI,CAAC;IACxC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gFAAgF;IAChF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;gFAG4E;IAC5E,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;uBAImB;IACnB,OAAO,CAAC,EAAE,mBAAmB,EAAE,CAAC;IAChC;;;;;;uEAMmE;IACnE,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B;;wDAEoD;IACpD,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,aAAa;IAC5B;4EACwE;IACxE,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAiED,wBAAsB,YAAY,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC,aAAa,CAAC,CA64B/E"}
|
package/dist/service.js
CHANGED
|
@@ -10,10 +10,8 @@
|
|
|
10
10
|
* { type: 'hello', payload: { agentId, model, version } }
|
|
11
11
|
* { type: 'event', payload: InvokeEvent } // see agents/types.ts
|
|
12
12
|
* { type: 'cdp-status', payload: { state, reason?, matchingTabUrl?, browser?, launching? } }
|
|
13
|
-
* { type: 'skill-saved', payload: { name, path } }
|
|
14
|
-
* { type: 'skill-exists', payload: { slug, existingPath } }
|
|
15
|
-
* { type: 'skills-list', payload: { skills: SkillSummary[] } }
|
|
16
13
|
* { type: 'specs-list', payload: { specs: SpecSummary[] } }
|
|
14
|
+
* { type: 'seeds-list', payload: { seeds: { name, note, signature, code, source }[] } }
|
|
17
15
|
* { type: 'spec-saved', payload: { name, path } }
|
|
18
16
|
* { type: 'spec-exists', payload: { slug, existingPath } }
|
|
19
17
|
* { type: 'case-csv-saved', payload: { name, path } }
|
|
@@ -31,11 +29,10 @@
|
|
|
31
29
|
* { type: 'check-cdp', payload: { pageUrl } } // "is this widget in the debug Chrome?"
|
|
32
30
|
* { type: 'launch-chrome', payload: { pageUrl } } // start debug Chrome, navigate to pageUrl
|
|
33
31
|
* { type: 'focus-debug', payload: { pageUrl } } // bringToFront the matching tab in debug Chrome
|
|
34
|
-
* { type: 'save-skill', payload: { name, description, steps, overwrite? } }
|
|
35
32
|
* { type: 'save-spec', payload: { name, description, steps, assertions?, overwrite? } }
|
|
36
33
|
* { type: 'save-case-csv', payload: { name, description, steps, assertions?, jiraProjectKey?, labels?, overwrite? } }
|
|
37
|
-
* { type: 'list-skills' }
|
|
38
34
|
* { type: 'list-specs' } // ask for every spec under __vibe_tests__/, with parsed JSDoc headers
|
|
35
|
+
* { type: 'list-seeds' } // ask for built-in + .hover/rules/ translation seeds (read-only)
|
|
39
36
|
* { type: 'list-agents' } // ask for the full agent registry + install status
|
|
40
37
|
* { type: 'switch-agent', payload: { agentId } } // set the service's current agent; broadcasts to all connections
|
|
41
38
|
*
|
|
@@ -49,18 +46,21 @@
|
|
|
49
46
|
* { type: 'list-modes' }
|
|
50
47
|
*/
|
|
51
48
|
import { WebSocketServer, WebSocket } from 'ws';
|
|
52
|
-
import {
|
|
49
|
+
import { runSession } from './runSession.js';
|
|
50
|
+
import { readConventions } from './service/conventions.js';
|
|
51
|
+
import { optimizeSpecWithAgent } from './specs/optimizeSpecWithAgent.js';
|
|
52
|
+
import { promoteOptimized, discardOptimized } from './specs/optimizeSpec.js';
|
|
53
53
|
import { listAgentAvailability, pickPrimaryAgent, } from './agents/detect.js';
|
|
54
54
|
import { getAgent } from './agents/registry.js';
|
|
55
55
|
import { getPreflight, invalidatePreflight } from './playwright/preflightCache.js';
|
|
56
56
|
import { resolveMcpConfig } from './playwright/resolveMcpConfig.js';
|
|
57
57
|
import { launchDebugChrome } from './playwright/launchChrome.js';
|
|
58
|
-
import { listSkills } from './skills/writeSkill.js';
|
|
59
58
|
import { listSpecs } from './specs/listSpecs.js';
|
|
59
|
+
import { readSeeds, BUILTIN_SEEDS } from './specs/seeds.js';
|
|
60
60
|
import { send, sendIfOpen } from './service/types.js';
|
|
61
61
|
import { buildCdpHint, buildCdpHintResume } from './service/cdpHint.js';
|
|
62
62
|
import { handleCheckCdp, handleLaunchChrome, handleFocusDebug, } from './service/cdpHandlers.js';
|
|
63
|
-
import { handleSaveArtifact,
|
|
63
|
+
import { handleSaveArtifact, SPEC_CONFIG, CASE_CSV_CONFIG, } from './service/saveHandlers.js';
|
|
64
64
|
import { CURRENT_API_VERSION, } from './plugin-api.js';
|
|
65
65
|
// ClientMessage + send moved to ./service/types.ts so the cdp + save
|
|
66
66
|
// handler modules can share them. See those files for the wire shape.
|
|
@@ -129,6 +129,11 @@ export async function startService(opts) {
|
|
|
129
129
|
const preferred = opts.agentId ?? process.env.HOVER_AGENT;
|
|
130
130
|
const primary = await pickPrimaryAgent(preferred);
|
|
131
131
|
let currentAgentId = primary?.descriptor.id ?? preferred ?? 'claude';
|
|
132
|
+
// Optional model API key the widget supplied (set-api-key). Held in memory
|
|
133
|
+
// for this service's lifetime only — never written to disk, never logged.
|
|
134
|
+
// Injected into the spawned CLI's env so a user without a logged-in
|
|
135
|
+
// subscription can drive Hover on their own key.
|
|
136
|
+
let currentApiKey = process.env.ANTHROPIC_API_KEY ?? process.env.OPENAI_API_KEY ?? undefined;
|
|
132
137
|
if (!primary) {
|
|
133
138
|
// Nothing installed — still bind so the widget can show a helpful
|
|
134
139
|
// "install one of these" dialog. Commands will fail with
|
|
@@ -147,6 +152,7 @@ export async function startService(opts) {
|
|
|
147
152
|
// so the user can hit Stop when they've seen enough. Pass maxBudgetUsd
|
|
148
153
|
// explicitly (or via the Vite plugin option) if a hard ceiling is needed.
|
|
149
154
|
const maxBudgetUsd = opts.maxBudgetUsd;
|
|
155
|
+
const optimizeMode = opts.optimizeMode ?? 'suggest';
|
|
150
156
|
const cdpUrl = opts.cdpUrl ?? 'http://localhost:9222';
|
|
151
157
|
const devRoot = opts.devRoot ?? process.cwd();
|
|
152
158
|
const wss = await pickAndBind('127.0.0.1', requestedPort, PORT_RETRIES);
|
|
@@ -384,7 +390,7 @@ export async function startService(opts) {
|
|
|
384
390
|
wss.on('connection', ws => {
|
|
385
391
|
send(ws, {
|
|
386
392
|
type: 'hello',
|
|
387
|
-
payload: { agentId: currentAgentId, model, version: PROTOCOL_VERSION },
|
|
393
|
+
payload: { agentId: currentAgentId, model, version: PROTOCOL_VERSION, optimizeMode },
|
|
388
394
|
});
|
|
389
395
|
// Send the agent list as a follow-up event so the widget can render the
|
|
390
396
|
// dropdown immediately on connect / reconnect (e.g. after HMR). The
|
|
@@ -531,13 +537,14 @@ export async function startService(opts) {
|
|
|
531
537
|
await broadcastAgents();
|
|
532
538
|
return;
|
|
533
539
|
}
|
|
534
|
-
if (msg.type === '
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
540
|
+
if (msg.type === 'set-api-key') {
|
|
541
|
+
// The widget supplies (or clears) a model API key. Stored in memory
|
|
542
|
+
// only and injected into the spawned CLI's env at invoke time — never
|
|
543
|
+
// persisted, never logged, never echoed back. Empty/missing clears it.
|
|
544
|
+
const key = msg.payload?.key;
|
|
545
|
+
currentApiKey = typeof key === 'string' && key.trim() ? key.trim() : undefined;
|
|
546
|
+
const envVar = getAgent(currentAgentId)?.apiKeyEnv;
|
|
547
|
+
send(ws, { type: 'api-key-status', payload: { hasKey: !!currentApiKey, envVar } });
|
|
541
548
|
return;
|
|
542
549
|
}
|
|
543
550
|
if (msg.type === 'list-specs') {
|
|
@@ -549,6 +556,21 @@ export async function startService(opts) {
|
|
|
549
556
|
send(ws, { type: 'specs-list', payload: { specs } });
|
|
550
557
|
return;
|
|
551
558
|
}
|
|
559
|
+
if (msg.type === 'list-seeds') {
|
|
560
|
+
// Widget's Seeds tab: show which translation seeds Hover sees — the
|
|
561
|
+
// built-in set + whatever the user dropped in <devRoot>/.hover/rules/.
|
|
562
|
+
// Read-only; users add seeds by hand (no download path).
|
|
563
|
+
const builtinNames = new Set(BUILTIN_SEEDS.map(s => s.name));
|
|
564
|
+
const seeds = (await readSeeds(devRoot)).map(s => ({
|
|
565
|
+
name: s.name,
|
|
566
|
+
note: s.note ?? '',
|
|
567
|
+
signature: s.signature,
|
|
568
|
+
code: s.example?.code ?? '',
|
|
569
|
+
source: builtinNames.has(s.name) ? 'builtin' : 'project',
|
|
570
|
+
}));
|
|
571
|
+
send(ws, { type: 'seeds-list', payload: { seeds } });
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
552
574
|
if (msg.type === 'save-spec') {
|
|
553
575
|
await handleSaveArtifact(ws, msg, devRoot, SPEC_CONFIG);
|
|
554
576
|
return;
|
|
@@ -557,6 +579,55 @@ export async function startService(opts) {
|
|
|
557
579
|
await handleSaveArtifact(ws, msg, devRoot, CASE_CSV_CONFIG);
|
|
558
580
|
return;
|
|
559
581
|
}
|
|
582
|
+
// Stage 7 (F7) widget flow: optimize a saved spec, then promote/discard
|
|
583
|
+
// the candidate after the human reviews the diff. optimizeSpecWithAgent
|
|
584
|
+
// spawns the codegen LLM (no browser, no MCP); the original spec is never
|
|
585
|
+
// touched until an explicit promote.
|
|
586
|
+
if (msg.type === 'optimize-spec') {
|
|
587
|
+
const slug = msg.payload?.slug;
|
|
588
|
+
if (typeof slug !== 'string' || !slug) {
|
|
589
|
+
send(ws, { type: 'error', payload: { message: 'optimize-spec: slug is required' } });
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
try {
|
|
593
|
+
const res = await optimizeSpecWithAgent(devRoot, slug, {
|
|
594
|
+
agentId: currentAgentId, model, maxBudgetUsd, apiKey: currentApiKey,
|
|
595
|
+
});
|
|
596
|
+
send(ws, { type: 'optimize-result', payload: { slug, original: res.original, candidate: res.code } });
|
|
597
|
+
}
|
|
598
|
+
catch (err) {
|
|
599
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
600
|
+
send(ws, { type: 'optimize-failed', payload: { slug, reason } });
|
|
601
|
+
}
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
if (msg.type === 'promote-optimized') {
|
|
605
|
+
const slug = msg.payload?.slug;
|
|
606
|
+
if (typeof slug !== 'string' || !slug) {
|
|
607
|
+
send(ws, { type: 'error', payload: { message: 'promote-optimized: slug is required' } });
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
try {
|
|
611
|
+
const path = await promoteOptimized(devRoot, slug);
|
|
612
|
+
send(ws, { type: 'optimized-promoted', payload: { slug, path } });
|
|
613
|
+
send(ws, { type: 'specs-list', payload: { specs: await listSpecs(devRoot) } });
|
|
614
|
+
}
|
|
615
|
+
catch (err) {
|
|
616
|
+
const m = err instanceof Error ? err.message : String(err);
|
|
617
|
+
send(ws, { type: 'error', payload: { message: `promote-optimized: ${m}` } });
|
|
618
|
+
}
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
if (msg.type === 'discard-optimized') {
|
|
622
|
+
const slug = msg.payload?.slug;
|
|
623
|
+
if (typeof slug !== 'string' || !slug) {
|
|
624
|
+
send(ws, { type: 'error', payload: { message: 'discard-optimized: slug is required' } });
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
await discardOptimized(devRoot, slug);
|
|
628
|
+
send(ws, { type: 'optimized-discarded', payload: { slug } });
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
560
631
|
// v0.12 — plugin-contributed save handlers. Lookup is O(plugins),
|
|
561
632
|
// which is fine because there's at most a handful of plugins ever
|
|
562
633
|
// loaded. Each plugin's manifest declares `saveHandlers[].type`
|
|
@@ -609,11 +680,11 @@ export async function startService(opts) {
|
|
|
609
680
|
? msg.payload.sessionId
|
|
610
681
|
: undefined;
|
|
611
682
|
// Re-record mode: when the client (widget Specs tab or hover CLI)
|
|
612
|
-
// passes `reRecord: { slug }`,
|
|
613
|
-
// into a
|
|
614
|
-
//
|
|
615
|
-
//
|
|
616
|
-
//
|
|
683
|
+
// passes `reRecord: { slug }`, runSession collects the tool_use events
|
|
684
|
+
// into a SpecStep[] and, on a clean finish, we overwrite the existing
|
|
685
|
+
// __vibe_tests__/<slug>.spec.ts. Same flow the widget uses for "Save as
|
|
686
|
+
// Spec", but the spec already exists and is being regenerated for the
|
|
687
|
+
// current UI.
|
|
617
688
|
const reRecordSlug = msg.payload && typeof msg.payload === 'object' && 'reRecord' in msg.payload
|
|
618
689
|
? msg.payload.reRecord?.slug
|
|
619
690
|
: undefined;
|
|
@@ -629,16 +700,6 @@ export async function startService(opts) {
|
|
|
629
700
|
busy = true;
|
|
630
701
|
cancelled = false;
|
|
631
702
|
inflight = new AbortController();
|
|
632
|
-
// Re-record step collector — populated as tool_use events stream by,
|
|
633
|
-
// consumed at session_end to overwrite the original spec. Empty unless
|
|
634
|
-
// reRecordSlug is set on this command. We seed with a synthetic
|
|
635
|
-
// `user` step so writeSpec's JSDoc Original-prompt: line carries the
|
|
636
|
-
// text the agent was actually given (which is the prompt we read out
|
|
637
|
-
// of the existing spec — the same one we're regenerating).
|
|
638
|
-
const reRecordSteps = [];
|
|
639
|
-
if (reRecordSlug) {
|
|
640
|
-
reRecordSteps.push({ kind: 'user', text });
|
|
641
|
-
}
|
|
642
703
|
try {
|
|
643
704
|
// Build the MCP config first — it's pure local file IO and lets
|
|
644
705
|
// us assert plugin-contributed servers landed in the config even
|
|
@@ -682,6 +743,15 @@ export async function startService(opts) {
|
|
|
682
743
|
let appendSystemPrompt = resumeSessionId
|
|
683
744
|
? buildCdpHintResume(cdp.tabs)
|
|
684
745
|
: buildCdpHint(cdp.tabs);
|
|
746
|
+
// Knowledge layer (F5): on the first turn, fold in the project's
|
|
747
|
+
// .hover/conventions.md (static, like cdpHint's rules — skipped on
|
|
748
|
+
// resume to keep the prompt cache intact). The service reads the file;
|
|
749
|
+
// the agent never gains filesystem access (D2).
|
|
750
|
+
if (!resumeSessionId) {
|
|
751
|
+
const conventions = await readConventions(devRoot);
|
|
752
|
+
if (conventions)
|
|
753
|
+
appendSystemPrompt = `${appendSystemPrompt}\n\n${conventions}`;
|
|
754
|
+
}
|
|
685
755
|
// Add plugin-contributed prompt additions whose scope includes the
|
|
686
756
|
// current mode (or '*' for always-on). Walks ALL loaded plugins,
|
|
687
757
|
// not just the active-mode plugin — a plugin that contributes
|
|
@@ -714,16 +784,9 @@ export async function startService(opts) {
|
|
|
714
784
|
}
|
|
715
785
|
// Snapshot the agent id so a switch-agent message during the run
|
|
716
786
|
// can't smear two agents across one invocation. (We also gate
|
|
717
|
-
// switch-agent on `busy`, but defense in depth.)
|
|
787
|
+
// switch-agent on `busy`, but defense in depth.) runSession gates the
|
|
788
|
+
// allow/deny lists on the agent's sandboxStrength internally.
|
|
718
789
|
const invokedAgentId = currentAgentId;
|
|
719
|
-
const invokedDescriptor = getAgent(invokedAgentId);
|
|
720
|
-
// Only Claude's `--allowedTools`/`--disallowedTools` flags are
|
|
721
|
-
// honoured — passing them to a soft-sandbox agent like codex is a
|
|
722
|
-
// no-op (its buildArgs ignores them). We still gate at the service
|
|
723
|
-
// layer for clarity: a hard-sandbox agent gets the tight allowlist,
|
|
724
|
-
// a soft one gets nothing and relies on its descriptor's built-in
|
|
725
|
-
// sandbox flags + developer_instructions.
|
|
726
|
-
const isHardSandbox = invokedDescriptor?.sandboxStrength === 'hard';
|
|
727
790
|
// Active mode's plugin-contributed MCP server ids — added to the
|
|
728
791
|
// hard-sandbox allow list so Claude can actually call them. Claude
|
|
729
792
|
// sanitises non-alphanumeric chars in the id when forming tool
|
|
@@ -743,87 +806,61 @@ export async function startService(opts) {
|
|
|
743
806
|
}
|
|
744
807
|
}
|
|
745
808
|
}
|
|
746
|
-
|
|
809
|
+
const runResult = await runSession({
|
|
747
810
|
agentId: invokedAgentId,
|
|
748
811
|
prompt: text,
|
|
749
812
|
sessionId: resumeSessionId,
|
|
750
813
|
mcpConfig,
|
|
751
|
-
// cwd = devRoot so
|
|
752
|
-
//
|
|
814
|
+
// cwd = devRoot so the agent runs against the project (and Claude
|
|
815
|
+
// Code reads its CLAUDE.md, if any).
|
|
753
816
|
cwd: devRoot,
|
|
754
817
|
appendSystemPrompt,
|
|
755
|
-
//
|
|
756
|
-
//
|
|
757
|
-
|
|
758
|
-
// the corresponding mode is active.
|
|
759
|
-
allowedTools: isHardSandbox
|
|
760
|
-
? ['mcp__playwright', 'Skill', ...activePluginMcpIds]
|
|
761
|
-
: undefined,
|
|
762
|
-
disallowedTools: isHardSandbox
|
|
763
|
-
? (invokedDescriptor?.defaultDisallowedTools
|
|
764
|
-
? [...invokedDescriptor.defaultDisallowedTools]
|
|
765
|
-
: undefined)
|
|
766
|
-
: undefined,
|
|
818
|
+
// mcp__playwright covers every browser tool; active-mode plugin MCP
|
|
819
|
+
// servers are appended. (Save-as-Skill retired → no Skill tool.)
|
|
820
|
+
allowedToolsExtra: activePluginMcpIds,
|
|
767
821
|
maxBudgetUsd,
|
|
768
822
|
model,
|
|
823
|
+
apiKey: currentApiKey,
|
|
769
824
|
signal: inflight.signal,
|
|
770
|
-
})
|
|
825
|
+
}, (ev) => {
|
|
771
826
|
if (cancelled || ws.readyState !== WebSocket.OPEN)
|
|
772
827
|
return;
|
|
773
828
|
send(ws, { type: 'event', payload: ev });
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
if (
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
829
|
+
});
|
|
830
|
+
// Re-record: write a fresh spec from the steps runSession accumulated
|
|
831
|
+
// (`user` → `step`* → `done`). Only on a clean, non-cancelled finish —
|
|
832
|
+
// a cancelled/aborted run throws out of runSession into the catch
|
|
833
|
+
// below, and an errored agent leaves the original spec untouched.
|
|
834
|
+
if (reRecordSlug && !cancelled) {
|
|
835
|
+
if (runResult.isError) {
|
|
836
|
+
sendIfOpen(ws, {
|
|
837
|
+
type: 'error',
|
|
838
|
+
payload: {
|
|
839
|
+
message: `Re-record failed: ${runResult.summary || 'agent reported an error'}. ` +
|
|
840
|
+
`Original spec left unchanged.`,
|
|
841
|
+
},
|
|
785
842
|
});
|
|
786
843
|
}
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
844
|
+
else {
|
|
845
|
+
try {
|
|
846
|
+
const { writeSpec } = await import('./specs/writeSpec.js');
|
|
847
|
+
const written = await writeSpec({
|
|
848
|
+
devRoot,
|
|
849
|
+
name: reRecordSlug,
|
|
850
|
+
steps: runResult.steps,
|
|
851
|
+
overwrite: true,
|
|
852
|
+
});
|
|
791
853
|
sendIfOpen(ws, {
|
|
792
|
-
type: '
|
|
793
|
-
payload: {
|
|
794
|
-
message: `Re-record failed: ${ev.summary ?? 'agent reported an error'}. ` +
|
|
795
|
-
`Original spec left unchanged.`,
|
|
796
|
-
},
|
|
854
|
+
type: 'spec-saved',
|
|
855
|
+
payload: { name: reRecordSlug, path: written.path },
|
|
797
856
|
});
|
|
798
857
|
}
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
}
|
|
805
|
-
// Overwrite. writeSpec uses the slug to name the file; we
|
|
806
|
-
// pass the original slug verbatim so the path is stable.
|
|
807
|
-
try {
|
|
808
|
-
const { writeSpec } = await import('./specs/writeSpec.js');
|
|
809
|
-
const result = await writeSpec({
|
|
810
|
-
devRoot,
|
|
811
|
-
name: reRecordSlug,
|
|
812
|
-
steps: reRecordSteps,
|
|
813
|
-
overwrite: true,
|
|
814
|
-
});
|
|
815
|
-
sendIfOpen(ws, {
|
|
816
|
-
type: 'spec-saved',
|
|
817
|
-
payload: { name: reRecordSlug, path: result.path },
|
|
818
|
-
});
|
|
819
|
-
}
|
|
820
|
-
catch (e) {
|
|
821
|
-
const m = e instanceof Error ? e.message : String(e);
|
|
822
|
-
sendIfOpen(ws, {
|
|
823
|
-
type: 'error',
|
|
824
|
-
payload: { message: `Re-record could not write spec: ${m}` },
|
|
825
|
-
});
|
|
826
|
-
}
|
|
858
|
+
catch (e) {
|
|
859
|
+
const m = e instanceof Error ? e.message : String(e);
|
|
860
|
+
sendIfOpen(ws, {
|
|
861
|
+
type: 'error',
|
|
862
|
+
payload: { message: `Re-record could not write spec: ${m}` },
|
|
863
|
+
});
|
|
827
864
|
}
|
|
828
865
|
}
|
|
829
866
|
}
|
|
@@ -1,8 +1,15 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Captured-step type.
|
|
3
|
+
*
|
|
4
|
+
* NOTE: Save-as-Skill (writing `.claude/skills/<slug>/SKILL.md` for agent
|
|
5
|
+
* replay) was retired — `spec` + ⟳ Re-record covers intent-driven replay, and
|
|
6
|
+
* "skill" collided with Claude Code's own skills concept. All that remains
|
|
7
|
+
* here is `SkillStep`: the serialized message shape from the widget's
|
|
8
|
+
* localStorage, which the whole spec pipeline (writeSpec, sidecar, listSpecs,
|
|
9
|
+
* Page-Object extraction) consumes as `SpecStep`. The file keeps its path so
|
|
10
|
+
* the many `import { SkillStep } from '../skills/writeSkill.js'` call sites
|
|
11
|
+
* don't churn; renaming to a neutral module is a separate mechanical pass.
|
|
12
|
+
*/
|
|
6
13
|
/**
|
|
7
14
|
* Serialized message shape from the widget's localStorage. Matches the
|
|
8
15
|
* `state.messages` schema in packages/widget-bootstrap/src/widget/client.js.
|
|
@@ -17,34 +24,4 @@ export interface SkillStep {
|
|
|
17
24
|
costUsd?: number;
|
|
18
25
|
summary?: string;
|
|
19
26
|
}
|
|
20
|
-
export interface WriteSkillOptions {
|
|
21
|
-
/** Directory under which `.claude/skills/<slug>/` is created. Usually the
|
|
22
|
-
* Vite project root (`server.config.root`). */
|
|
23
|
-
devRoot: string;
|
|
24
|
-
name: string;
|
|
25
|
-
description?: string;
|
|
26
|
-
steps: SkillStep[];
|
|
27
|
-
/** If false (default), throws SkillExistsError when a skill with the same
|
|
28
|
-
* slug already exists. If true, overwrites unconditionally. The widget
|
|
29
|
-
* uses the two paths to give the user a confirm dialog. */
|
|
30
|
-
overwrite?: boolean;
|
|
31
|
-
}
|
|
32
|
-
export interface WriteSkillResult {
|
|
33
|
-
path: string;
|
|
34
|
-
slug: string;
|
|
35
|
-
}
|
|
36
|
-
export declare function writeSkill(opts: WriteSkillOptions): Promise<WriteSkillResult>;
|
|
37
|
-
export interface SkillSummary {
|
|
38
|
-
slug: string;
|
|
39
|
-
name: string;
|
|
40
|
-
description: string;
|
|
41
|
-
path: string;
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* List skills under <devRoot>/.claude/skills/, reading the YAML frontmatter
|
|
45
|
-
* of each SKILL.md for `name` and `description`. Malformed entries are
|
|
46
|
-
* silently skipped — better to show 9 valid skills than refuse to render
|
|
47
|
-
* because one is broken. Hand-edited skills are first-class.
|
|
48
|
-
*/
|
|
49
|
-
export declare function listSkills(devRoot: string): Promise<SkillSummary[]>;
|
|
50
27
|
//# sourceMappingURL=writeSkill.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"writeSkill.d.ts","sourceRoot":"","sources":["../../src/skills/writeSkill.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"writeSkill.d.ts","sourceRoot":"","sources":["../../src/skills/writeSkill.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH;;;GAGG;AACH,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,GAAG,QAAQ,GAAG,MAAM,GAAG,IAAI,GAAG,MAAM,CAAC;IACjD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB"}
|
|
@@ -1,169 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Captured-step type.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* - The original user prompt + AI outcome are preserved as prose. If the
|
|
13
|
-
* page changed and the literal selectors no longer apply, the agent has
|
|
14
|
-
* enough context to adapt rather than fail.
|
|
15
|
-
*/
|
|
16
|
-
import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises';
|
|
17
|
-
import { existsSync } from 'node:fs';
|
|
18
|
-
import { join } from 'node:path';
|
|
19
|
-
export class SkillExistsError extends Error {
|
|
20
|
-
slug;
|
|
21
|
-
path;
|
|
22
|
-
constructor(slug, path) {
|
|
23
|
-
super(`Skill "${slug}" already exists at ${path}`);
|
|
24
|
-
this.slug = slug;
|
|
25
|
-
this.path = path;
|
|
26
|
-
this.name = 'SkillExistsError';
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
export async function writeSkill(opts) {
|
|
30
|
-
const slug = slugify(opts.name);
|
|
31
|
-
if (!slug) {
|
|
32
|
-
throw new Error('skill name must contain at least one alphanumeric character');
|
|
33
|
-
}
|
|
34
|
-
if (!opts.steps.some(s => s.kind === 'step')) {
|
|
35
|
-
throw new Error('skill must contain at least one tool_use step to replay');
|
|
36
|
-
}
|
|
37
|
-
const dir = join(opts.devRoot, '.claude', 'skills', slug);
|
|
38
|
-
const path = join(dir, 'SKILL.md');
|
|
39
|
-
if (!opts.overwrite && existsSync(path)) {
|
|
40
|
-
throw new SkillExistsError(slug, path);
|
|
41
|
-
}
|
|
42
|
-
await mkdir(dir, { recursive: true });
|
|
43
|
-
const md = renderSkill(slug, opts.description ?? '', opts.steps);
|
|
44
|
-
await writeFile(path, md, 'utf-8');
|
|
45
|
-
return { path, slug };
|
|
46
|
-
}
|
|
47
|
-
function slugify(name) {
|
|
48
|
-
return name
|
|
49
|
-
.toLowerCase()
|
|
50
|
-
.trim()
|
|
51
|
-
.replace(/[^a-z0-9]+/g, '-')
|
|
52
|
-
.replace(/^-+|-+$/g, '');
|
|
53
|
-
}
|
|
54
|
-
function renderSkill(slug, description, steps) {
|
|
55
|
-
const userMsg = steps.find(s => s.kind === 'user');
|
|
56
|
-
const doneMsg = [...steps].reverse().find(s => s.kind === 'done');
|
|
57
|
-
const toolSteps = steps.filter(s => s.kind === 'step');
|
|
58
|
-
const out = [];
|
|
59
|
-
out.push('---');
|
|
60
|
-
out.push(`name: ${slug}`);
|
|
61
|
-
// YAML description — quote if it contains anything that could confuse the parser
|
|
62
|
-
out.push(`description: ${yamlString(description || slug)}`);
|
|
63
|
-
out.push('---');
|
|
64
|
-
out.push('');
|
|
65
|
-
if (userMsg?.text) {
|
|
66
|
-
out.push('## Original intent');
|
|
67
|
-
out.push('');
|
|
68
|
-
out.push(blockquote(userMsg.text));
|
|
69
|
-
out.push('');
|
|
70
|
-
}
|
|
71
|
-
out.push('## Replay steps');
|
|
72
|
-
out.push('');
|
|
73
|
-
out.push('Replay these steps using the `mcp__playwright` tools, in order. ' +
|
|
74
|
-
'If a literal selector id (e.g. `e15`) no longer matches, interpret the ' +
|
|
75
|
-
'natural-language element description instead — selector ids regenerate on every snapshot.');
|
|
76
|
-
out.push('');
|
|
77
|
-
out.push('Do not narrate each step, do not summarize at the end. Hover surfaces ' +
|
|
78
|
-
'tool calls + the final result to the user automatically — extra commentary is noise.');
|
|
79
|
-
out.push('');
|
|
80
|
-
toolSteps.forEach((step, i) => {
|
|
81
|
-
const tool = step.tool ?? '(unknown)';
|
|
82
|
-
const inputStr = JSON.stringify(step.input ?? {});
|
|
83
|
-
const truncated = inputStr.length > 240 ? inputStr.slice(0, 237) + '…' : inputStr;
|
|
84
|
-
out.push(`${i + 1}. \`${tool}\` — \`${truncated}\``);
|
|
85
|
-
});
|
|
86
|
-
out.push('');
|
|
87
|
-
if (doneMsg?.summary) {
|
|
88
|
-
out.push('## Original outcome');
|
|
89
|
-
out.push('');
|
|
90
|
-
out.push(doneMsg.summary.trim());
|
|
91
|
-
out.push('');
|
|
92
|
-
}
|
|
93
|
-
return out.join('\n');
|
|
94
|
-
}
|
|
95
|
-
function yamlString(s) {
|
|
96
|
-
// Cheap quoting — if it has YAML-significant chars, double-quote and escape.
|
|
97
|
-
if (/[:#\n"'\\]/.test(s)) {
|
|
98
|
-
return `"${s.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
|
99
|
-
}
|
|
100
|
-
return s;
|
|
101
|
-
}
|
|
102
|
-
function blockquote(s) {
|
|
103
|
-
return s
|
|
104
|
-
.split('\n')
|
|
105
|
-
.map(line => `> ${line}`)
|
|
106
|
-
.join('\n');
|
|
107
|
-
}
|
|
108
|
-
/**
|
|
109
|
-
* List skills under <devRoot>/.claude/skills/, reading the YAML frontmatter
|
|
110
|
-
* of each SKILL.md for `name` and `description`. Malformed entries are
|
|
111
|
-
* silently skipped — better to show 9 valid skills than refuse to render
|
|
112
|
-
* because one is broken. Hand-edited skills are first-class.
|
|
4
|
+
* NOTE: Save-as-Skill (writing `.claude/skills/<slug>/SKILL.md` for agent
|
|
5
|
+
* replay) was retired — `spec` + ⟳ Re-record covers intent-driven replay, and
|
|
6
|
+
* "skill" collided with Claude Code's own skills concept. All that remains
|
|
7
|
+
* here is `SkillStep`: the serialized message shape from the widget's
|
|
8
|
+
* localStorage, which the whole spec pipeline (writeSpec, sidecar, listSpecs,
|
|
9
|
+
* Page-Object extraction) consumes as `SpecStep`. The file keeps its path so
|
|
10
|
+
* the many `import { SkillStep } from '../skills/writeSkill.js'` call sites
|
|
11
|
+
* don't churn; renaming to a neutral module is a separate mechanical pass.
|
|
113
12
|
*/
|
|
114
|
-
export
|
|
115
|
-
const root = join(devRoot, '.claude', 'skills');
|
|
116
|
-
let entries;
|
|
117
|
-
try {
|
|
118
|
-
entries = await readdir(root);
|
|
119
|
-
}
|
|
120
|
-
catch {
|
|
121
|
-
return [];
|
|
122
|
-
}
|
|
123
|
-
const skills = [];
|
|
124
|
-
for (const slug of entries) {
|
|
125
|
-
if (slug.startsWith('.'))
|
|
126
|
-
continue;
|
|
127
|
-
const path = join(root, slug, 'SKILL.md');
|
|
128
|
-
let content;
|
|
129
|
-
try {
|
|
130
|
-
content = await readFile(path, 'utf-8');
|
|
131
|
-
}
|
|
132
|
-
catch {
|
|
133
|
-
continue;
|
|
134
|
-
}
|
|
135
|
-
const fm = parseFrontmatter(content);
|
|
136
|
-
skills.push({
|
|
137
|
-
slug,
|
|
138
|
-
name: fm.name ?? slug,
|
|
139
|
-
description: fm.description ?? '',
|
|
140
|
-
path,
|
|
141
|
-
});
|
|
142
|
-
}
|
|
143
|
-
// Sort newest-first by mtime would require fs.stat; alpha is fine and stable.
|
|
144
|
-
skills.sort((a, b) => a.slug.localeCompare(b.slug));
|
|
145
|
-
return skills;
|
|
146
|
-
}
|
|
147
|
-
function parseFrontmatter(content) {
|
|
148
|
-
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
149
|
-
if (!match)
|
|
150
|
-
return {};
|
|
151
|
-
const out = {};
|
|
152
|
-
for (const line of match[1].split('\n')) {
|
|
153
|
-
const m = line.match(/^(\w+)\s*:\s*(.+)$/);
|
|
154
|
-
if (!m)
|
|
155
|
-
continue;
|
|
156
|
-
let value = m[2].trim();
|
|
157
|
-
// Strip wrapping quotes if present (yamlString() in renderSkill quotes
|
|
158
|
-
// strings with YAML-significant chars).
|
|
159
|
-
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
160
|
-
(value.startsWith("'") && value.endsWith("'"))) {
|
|
161
|
-
value = value
|
|
162
|
-
.slice(1, -1)
|
|
163
|
-
.replace(/\\"/g, '"')
|
|
164
|
-
.replace(/\\\\/g, '\\');
|
|
165
|
-
}
|
|
166
|
-
out[m[1]] = value;
|
|
167
|
-
}
|
|
168
|
-
return out;
|
|
169
|
-
}
|
|
13
|
+
export {};
|