@hover-dev/core 0.2.0 → 0.2.3
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 +1 -1
- package/dist/agents/claude.d.ts.map +1 -1
- package/dist/agents/claude.js +25 -17
- package/dist/agents/codex.d.ts +19 -0
- package/dist/agents/codex.d.ts.map +1 -0
- package/dist/agents/codex.js +210 -0
- package/dist/agents/detect.d.ts +27 -1
- package/dist/agents/detect.d.ts.map +1 -1
- package/dist/agents/detect.js +46 -3
- package/dist/agents/invoke.d.ts.map +1 -1
- package/dist/agents/invoke.js +21 -7
- package/dist/agents/registry.d.ts +10 -5
- package/dist/agents/registry.d.ts.map +1 -1
- package/dist/agents/registry.js +14 -5
- package/dist/agents/types.d.ts +71 -1
- package/dist/agents/types.d.ts.map +1 -1
- package/dist/playwright/cdpStatus.d.ts.map +1 -1
- package/dist/playwright/cdpStatus.js +32 -9
- package/dist/playwright/raiseWindow.d.ts +10 -0
- package/dist/playwright/raiseWindow.d.ts.map +1 -0
- package/dist/playwright/raiseWindow.js +124 -0
- package/dist/service/cdpHandlers.d.ts +39 -0
- package/dist/service/cdpHandlers.d.ts.map +1 -0
- package/dist/service/cdpHandlers.js +82 -0
- package/dist/service/cdpHint.d.ts +20 -0
- package/dist/service/cdpHint.d.ts.map +1 -0
- package/dist/service/cdpHint.js +86 -0
- package/dist/service/saveHandlers.d.ts +55 -0
- package/dist/service/saveHandlers.d.ts.map +1 -0
- package/dist/service/saveHandlers.js +80 -0
- package/dist/service/types.d.ts +41 -0
- package/dist/service/types.d.ts.map +1 -0
- package/dist/service/types.js +14 -0
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +141 -235
- package/package.json +1 -1
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { chromium } from 'playwright-core';
|
|
15
15
|
import { preflightCDP } from './preflight.js';
|
|
16
|
+
import { findCdpPid, raiseChromeWindow } from './raiseWindow.js';
|
|
16
17
|
/**
|
|
17
18
|
* Parse a page URL down to its origin (protocol + host + port). We compare
|
|
18
19
|
* by origin, not full URL — the user might be on /login while the debug
|
|
@@ -72,25 +73,47 @@ export async function focusDebugTab(cdpUrl, pageUrl) {
|
|
|
72
73
|
const msg = err instanceof Error ? err.message : String(err);
|
|
73
74
|
return { ok: false, reason: `couldn't connect to CDP at ${cdpUrl}: ${msg}` };
|
|
74
75
|
}
|
|
76
|
+
let focusedUrl;
|
|
75
77
|
try {
|
|
76
78
|
const pages = browser.contexts().flatMap(c => c.pages());
|
|
77
79
|
const match = pages.find(p => originOf(p.url()) === wantOrigin);
|
|
78
80
|
if (match) {
|
|
79
81
|
await match.bringToFront();
|
|
80
|
-
|
|
82
|
+
focusedUrl = match.url();
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
// No tab on the dev origin yet — open one so the widget appears.
|
|
86
|
+
const context = browser.contexts()[0] ?? (await browser.newContext());
|
|
87
|
+
const page = await context.newPage();
|
|
88
|
+
await page.goto(pageUrl, { waitUntil: 'domcontentloaded' });
|
|
89
|
+
await page.bringToFront();
|
|
90
|
+
focusedUrl = page.url();
|
|
81
91
|
}
|
|
82
|
-
// No tab on the dev origin yet — open one so the widget appears.
|
|
83
|
-
const context = browser.contexts()[0] ?? (await browser.newContext());
|
|
84
|
-
const page = await context.newPage();
|
|
85
|
-
await page.goto(pageUrl, { waitUntil: 'domcontentloaded' });
|
|
86
|
-
await page.bringToFront();
|
|
87
|
-
return { ok: true, focusedUrl: page.url() };
|
|
88
92
|
}
|
|
89
93
|
catch (err) {
|
|
94
|
+
await browser.close().catch(() => { });
|
|
90
95
|
const msg = err instanceof Error ? err.message : String(err);
|
|
91
96
|
return { ok: false, reason: `bringToFront failed: ${msg}` };
|
|
92
97
|
}
|
|
93
|
-
|
|
94
|
-
|
|
98
|
+
await browser.close().catch(() => { });
|
|
99
|
+
// CDP-level bringToFront only activates the tab inside the Chrome process;
|
|
100
|
+
// on macOS in particular the Chrome *window* stays buried if it wasn't
|
|
101
|
+
// foreground already. Raise the OS window too. Best-effort, never fatal.
|
|
102
|
+
const port = portFromCdpUrl(cdpUrl);
|
|
103
|
+
if (port !== null) {
|
|
104
|
+
const pid = await findCdpPid(port);
|
|
105
|
+
if (pid !== null)
|
|
106
|
+
await raiseChromeWindow(pid);
|
|
107
|
+
}
|
|
108
|
+
return { ok: true, focusedUrl };
|
|
109
|
+
}
|
|
110
|
+
function portFromCdpUrl(cdpUrl) {
|
|
111
|
+
try {
|
|
112
|
+
const u = new URL(cdpUrl);
|
|
113
|
+
const port = Number.parseInt(u.port, 10);
|
|
114
|
+
return Number.isInteger(port) && port > 0 ? port : null;
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return null;
|
|
95
118
|
}
|
|
96
119
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Find the OS PID of the process listening on the given TCP port.
|
|
3
|
+
* Returns null if nothing is listening or the lookup tool isn't available.
|
|
4
|
+
*/
|
|
5
|
+
export declare function findCdpPid(port: number): Promise<number | null>;
|
|
6
|
+
/**
|
|
7
|
+
* Raise the Chrome window owned by `pid` to the OS foreground. Best-effort.
|
|
8
|
+
*/
|
|
9
|
+
export declare function raiseChromeWindow(pid: number): Promise<void>;
|
|
10
|
+
//# sourceMappingURL=raiseWindow.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"raiseWindow.d.ts","sourceRoot":"","sources":["../../src/playwright/raiseWindow.ts"],"names":[],"mappings":"AAyBA;;;GAGG;AACH,wBAAsB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CA8BrE;AAED;;GAEG;AACH,wBAAsB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAwClE"}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Raise the OS-level Chrome window to the foreground.
|
|
3
|
+
*
|
|
4
|
+
* Why this exists: CDP's `Page.bringToFront()` and `Target.activateTarget`
|
|
5
|
+
* only reorder tabs *inside* the Chrome process — they do not raise the
|
|
6
|
+
* Chrome application window in the OS's window stack. When the user clicks
|
|
7
|
+
* "Switch me to it" from a widget hosted in a different window, the tab
|
|
8
|
+
* activates correctly inside the (possibly background) debug Chrome, but
|
|
9
|
+
* the window stays buried. The user then has to manually click the Chrome
|
|
10
|
+
* Dock icon / Alt-Tab to it, which defeats the point of the button.
|
|
11
|
+
*
|
|
12
|
+
* Fix: after `bringToFront()`, run an OS-specific command that raises the
|
|
13
|
+
* specific Chrome *process* (matched by PID, found from the CDP port via
|
|
14
|
+
* `lsof` / `netstat`). PID-matching is critical — the user's own primary
|
|
15
|
+
* Chrome and Hover's debug Chrome are both "Google Chrome" to AppleScript,
|
|
16
|
+
* so raising by app name would risk activating the wrong window.
|
|
17
|
+
*
|
|
18
|
+
* Best-effort and non-blocking — if the helper fails, we still leave the
|
|
19
|
+
* tab correctly focused inside the debug Chrome and the user can click
|
|
20
|
+
* over manually like before. Logging is to stderr only; this never throws
|
|
21
|
+
* back to the caller.
|
|
22
|
+
*/
|
|
23
|
+
import { spawn } from 'node:child_process';
|
|
24
|
+
import { platform } from 'node:os';
|
|
25
|
+
/**
|
|
26
|
+
* Find the OS PID of the process listening on the given TCP port.
|
|
27
|
+
* Returns null if nothing is listening or the lookup tool isn't available.
|
|
28
|
+
*/
|
|
29
|
+
export async function findCdpPid(port) {
|
|
30
|
+
const os = platform();
|
|
31
|
+
if (os === 'darwin' || os === 'linux') {
|
|
32
|
+
// -t prints just PIDs, -sTCP:LISTEN filters to listeners (otherwise
|
|
33
|
+
// every client connection's PID would show up too).
|
|
34
|
+
const out = await runCapture('lsof', ['-tiTCP:' + port, '-sTCP:LISTEN']);
|
|
35
|
+
if (!out)
|
|
36
|
+
return null;
|
|
37
|
+
// lsof may print multiple lines if forked workers also hold the port;
|
|
38
|
+
// take the first numeric one.
|
|
39
|
+
for (const line of out.split('\n')) {
|
|
40
|
+
const pid = Number.parseInt(line.trim(), 10);
|
|
41
|
+
if (Number.isInteger(pid) && pid > 0)
|
|
42
|
+
return pid;
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
if (os === 'win32') {
|
|
47
|
+
// `netstat -ano` columns: Proto Local Foreign State PID
|
|
48
|
+
const out = await runCapture('netstat', ['-ano']);
|
|
49
|
+
if (!out)
|
|
50
|
+
return null;
|
|
51
|
+
for (const line of out.split('\n')) {
|
|
52
|
+
// Match `TCP 127.0.0.1:9222 0.0.0.0:0 LISTENING 1234`
|
|
53
|
+
const m = line.match(/^\s*TCP\s+\S+:(\d+)\s+\S+\s+LISTENING\s+(\d+)\s*$/i);
|
|
54
|
+
if (m && Number(m[1]) === port)
|
|
55
|
+
return Number(m[2]);
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Raise the Chrome window owned by `pid` to the OS foreground. Best-effort.
|
|
63
|
+
*/
|
|
64
|
+
export async function raiseChromeWindow(pid) {
|
|
65
|
+
const os = platform();
|
|
66
|
+
try {
|
|
67
|
+
if (os === 'darwin') {
|
|
68
|
+
// System Events can frontmost any process by its unix PID, regardless
|
|
69
|
+
// of app bundle. This works even when several "Google Chrome"
|
|
70
|
+
// processes coexist (user's primary + Hover's debug).
|
|
71
|
+
await runDetached('osascript', [
|
|
72
|
+
'-e',
|
|
73
|
+
`tell application "System Events" to set frontmost of (first process whose unix id is ${pid}) to true`,
|
|
74
|
+
]);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (os === 'linux') {
|
|
78
|
+
// wmctrl is the most common helper for X11; not always installed,
|
|
79
|
+
// but the alternative (xdotool) needs the same dependency story.
|
|
80
|
+
// We try wmctrl with the PID match; if it isn't installed the
|
|
81
|
+
// outer try/catch swallows the ENOENT and we degrade gracefully.
|
|
82
|
+
await runDetached('wmctrl', ['-ia', String(pid)]);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (os === 'win32') {
|
|
86
|
+
// PowerShell is bundled with Windows 10+. AppActivate is best-effort:
|
|
87
|
+
// it requires the target to have a visible main window, which a
|
|
88
|
+
// headless-less Chrome with a tab open satisfies.
|
|
89
|
+
const ps = `$p = Get-Process -Id ${pid} -ErrorAction SilentlyContinue; ` +
|
|
90
|
+
`if ($p) { ` +
|
|
91
|
+
` Add-Type -AssemblyName Microsoft.VisualBasic; ` +
|
|
92
|
+
` [Microsoft.VisualBasic.Interaction]::AppActivate($p.Id) ` +
|
|
93
|
+
`}`;
|
|
94
|
+
await runDetached('powershell', ['-NoProfile', '-Command', ps]);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
// Best-effort. CDP-level bringToFront already ran; user can still
|
|
100
|
+
// click the Chrome window manually.
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function runCapture(cmd, args) {
|
|
104
|
+
return new Promise(resolve => {
|
|
105
|
+
let out = '';
|
|
106
|
+
const child = spawn(cmd, args, { stdio: ['ignore', 'pipe', 'ignore'] });
|
|
107
|
+
child.stdout.on('data', chunk => {
|
|
108
|
+
out += chunk.toString();
|
|
109
|
+
});
|
|
110
|
+
child.on('error', () => resolve(null));
|
|
111
|
+
child.on('close', code => resolve(code === 0 ? out : null));
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
function runDetached(cmd, args) {
|
|
115
|
+
return new Promise((resolve, reject) => {
|
|
116
|
+
const child = spawn(cmd, args, { stdio: 'ignore' });
|
|
117
|
+
child.on('error', reject);
|
|
118
|
+
child.on('close', code => {
|
|
119
|
+
// Don't treat non-zero as fatal — caller already wraps in try/catch.
|
|
120
|
+
resolve();
|
|
121
|
+
void code;
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CDP-related WebSocket message handlers.
|
|
3
|
+
*
|
|
4
|
+
* check-cdp → checkCdpStatus → emit cdp-status
|
|
5
|
+
* launch-chrome → emit "launching" placeholder → launchDebugChrome →
|
|
6
|
+
* re-check status → emit cdp-status
|
|
7
|
+
* focus-debug → focusDebugTab → no message on success (the widget the
|
|
8
|
+
* user is about to focus runs its own check-cdp anyway)
|
|
9
|
+
*
|
|
10
|
+
* Extracted from service.ts during the v0.2.x refactor pass so the main
|
|
11
|
+
* file can be a thin orchestrator.
|
|
12
|
+
*/
|
|
13
|
+
import type { WebSocket } from 'ws';
|
|
14
|
+
import { type ClientMessage } from './types.js';
|
|
15
|
+
/**
|
|
16
|
+
* "Is this widget running inside the debug Chrome?" The widget asks this on
|
|
17
|
+
* connect (and after every status-changing event) so it can render itself as
|
|
18
|
+
* either:
|
|
19
|
+
* - same-window → normal, drives the page
|
|
20
|
+
* - wrong-window → disabled, with a "use the other window" notice
|
|
21
|
+
* - no-cdp → enabled but click triggers launch-chrome instead
|
|
22
|
+
*/
|
|
23
|
+
export declare function handleCheckCdp(ws: WebSocket, msg: ClientMessage, cdpUrl: string): Promise<void>;
|
|
24
|
+
/**
|
|
25
|
+
* Launch a debug Chrome navigated to `pageUrl`, then re-check status. The
|
|
26
|
+
* re-check usually returns 'wrong-window' (because the widget asking is in
|
|
27
|
+
* the user's regular Chrome, not the freshly-launched one) — the widget then
|
|
28
|
+
* displays the "use the other window" state.
|
|
29
|
+
*/
|
|
30
|
+
export declare function handleLaunchChrome(ws: WebSocket, msg: ClientMessage, cdpUrl: string): Promise<void>;
|
|
31
|
+
/**
|
|
32
|
+
* bringToFront the debug-Chrome tab matching `pageUrl`'s origin (or open one
|
|
33
|
+
* if none exists). Used by the wrong-window UI's "switch to debug Chrome"
|
|
34
|
+
* button. Doesn't return cdp-status — bringToFront doesn't change anything
|
|
35
|
+
* the widget cares about, and the widget the user is about to focus is a
|
|
36
|
+
* different page (and will run its own check-cdp on its own ws connection).
|
|
37
|
+
*/
|
|
38
|
+
export declare function handleFocusDebug(ws: WebSocket, msg: ClientMessage, cdpUrl: string): Promise<void>;
|
|
39
|
+
//# sourceMappingURL=cdpHandlers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cdpHandlers.d.ts","sourceRoot":"","sources":["../../src/service/cdpHandlers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,IAAI,CAAC;AAGpC,OAAO,EAAQ,KAAK,aAAa,EAAE,MAAM,YAAY,CAAC;AAEtD;;;;;;;GAOG;AACH,wBAAsB,cAAc,CAClC,EAAE,EAAE,SAAS,EACb,GAAG,EAAE,aAAa,EAClB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,IAAI,CAAC,CAQf;AAED;;;;;GAKG;AACH,wBAAsB,kBAAkB,CACtC,EAAE,EAAE,SAAS,EACb,GAAG,EAAE,aAAa,EAClB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,IAAI,CAAC,CAyBf;AAED;;;;;;GAMG;AACH,wBAAsB,gBAAgB,CACpC,EAAE,EAAE,SAAS,EACb,GAAG,EAAE,aAAa,EAClB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,IAAI,CAAC,CAUf"}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CDP-related WebSocket message handlers.
|
|
3
|
+
*
|
|
4
|
+
* check-cdp → checkCdpStatus → emit cdp-status
|
|
5
|
+
* launch-chrome → emit "launching" placeholder → launchDebugChrome →
|
|
6
|
+
* re-check status → emit cdp-status
|
|
7
|
+
* focus-debug → focusDebugTab → no message on success (the widget the
|
|
8
|
+
* user is about to focus runs its own check-cdp anyway)
|
|
9
|
+
*
|
|
10
|
+
* Extracted from service.ts during the v0.2.x refactor pass so the main
|
|
11
|
+
* file can be a thin orchestrator.
|
|
12
|
+
*/
|
|
13
|
+
import { checkCdpStatus, focusDebugTab } from '../playwright/cdpStatus.js';
|
|
14
|
+
import { launchDebugChrome } from '../playwright/launchChrome.js';
|
|
15
|
+
import { send } from './types.js';
|
|
16
|
+
/**
|
|
17
|
+
* "Is this widget running inside the debug Chrome?" The widget asks this on
|
|
18
|
+
* connect (and after every status-changing event) so it can render itself as
|
|
19
|
+
* either:
|
|
20
|
+
* - same-window → normal, drives the page
|
|
21
|
+
* - wrong-window → disabled, with a "use the other window" notice
|
|
22
|
+
* - no-cdp → enabled but click triggers launch-chrome instead
|
|
23
|
+
*/
|
|
24
|
+
export async function handleCheckCdp(ws, msg, cdpUrl) {
|
|
25
|
+
const pageUrl = msg.payload?.pageUrl;
|
|
26
|
+
if (typeof pageUrl !== 'string' || !pageUrl) {
|
|
27
|
+
send(ws, { type: 'error', payload: { message: 'check-cdp: pageUrl is required' } });
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const status = await checkCdpStatus(cdpUrl, pageUrl);
|
|
31
|
+
send(ws, { type: 'cdp-status', payload: status });
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Launch a debug Chrome navigated to `pageUrl`, then re-check status. The
|
|
35
|
+
* re-check usually returns 'wrong-window' (because the widget asking is in
|
|
36
|
+
* the user's regular Chrome, not the freshly-launched one) — the widget then
|
|
37
|
+
* displays the "use the other window" state.
|
|
38
|
+
*/
|
|
39
|
+
export async function handleLaunchChrome(ws, msg, cdpUrl) {
|
|
40
|
+
const pageUrl = msg.payload?.pageUrl;
|
|
41
|
+
if (typeof pageUrl !== 'string' || !pageUrl) {
|
|
42
|
+
send(ws, { type: 'error', payload: { message: 'launch-chrome: pageUrl is required' } });
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
// Tell the widget we're launching so it can render a spinner immediately —
|
|
46
|
+
// findChromeBinary + spawn + ready-poll can take a few seconds.
|
|
47
|
+
send(ws, { type: 'cdp-status', payload: { state: 'no-cdp', launching: true } });
|
|
48
|
+
const port = (() => {
|
|
49
|
+
try {
|
|
50
|
+
return Number(new URL(cdpUrl).port) || 9222;
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return 9222;
|
|
54
|
+
}
|
|
55
|
+
})();
|
|
56
|
+
const result = await launchDebugChrome({ url: pageUrl, port });
|
|
57
|
+
if (!result.ok) {
|
|
58
|
+
send(ws, { type: 'cdp-status', payload: { state: 'no-cdp', reason: result.reason } });
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
// Re-check after launch so the widget gets the real status.
|
|
62
|
+
const status = await checkCdpStatus(cdpUrl, pageUrl);
|
|
63
|
+
send(ws, { type: 'cdp-status', payload: status });
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* bringToFront the debug-Chrome tab matching `pageUrl`'s origin (or open one
|
|
67
|
+
* if none exists). Used by the wrong-window UI's "switch to debug Chrome"
|
|
68
|
+
* button. Doesn't return cdp-status — bringToFront doesn't change anything
|
|
69
|
+
* the widget cares about, and the widget the user is about to focus is a
|
|
70
|
+
* different page (and will run its own check-cdp on its own ws connection).
|
|
71
|
+
*/
|
|
72
|
+
export async function handleFocusDebug(ws, msg, cdpUrl) {
|
|
73
|
+
const pageUrl = msg.payload?.pageUrl;
|
|
74
|
+
if (typeof pageUrl !== 'string' || !pageUrl) {
|
|
75
|
+
send(ws, { type: 'error', payload: { message: 'focus-debug: pageUrl is required' } });
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const result = await focusDebugTab(cdpUrl, pageUrl);
|
|
79
|
+
if (!result.ok) {
|
|
80
|
+
send(ws, { type: 'error', payload: { message: `focus-debug: ${result.reason}` } });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* System-prompt addendum sent to the agent on every command.
|
|
3
|
+
*
|
|
4
|
+
* Two roles:
|
|
5
|
+
* 1. Navigation rules — the most failure-prone agent behaviours are
|
|
6
|
+
* `browser_navigate` to same-origin paths (kills the widget) and
|
|
7
|
+
* reading the JS bundle for credentials. We tell the agent both
|
|
8
|
+
* mistakes by name, including the actual origin to forbid.
|
|
9
|
+
* 2. Narration format — how the widget renders the run depends on the
|
|
10
|
+
* agent emitting short imperative one-liners before each logical
|
|
11
|
+
* step. The good/bad examples are present-tense and 3–8 words.
|
|
12
|
+
*
|
|
13
|
+
* Lives in its own file because this string is the most-tuned text in the
|
|
14
|
+
* repo and the easiest to break with a typo. Tests can import directly.
|
|
15
|
+
*/
|
|
16
|
+
export declare function buildCdpHint(tabs: {
|
|
17
|
+
url: string;
|
|
18
|
+
title?: string;
|
|
19
|
+
}[]): string;
|
|
20
|
+
//# sourceMappingURL=cdpHint.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cdpHint.d.ts","sourceRoot":"","sources":["../../src/service/cdpHint.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,wBAAgB,YAAY,CAAC,IAAI,EAAE;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,EAAE,GAAG,MAAM,CAmE5E"}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* System-prompt addendum sent to the agent on every command.
|
|
3
|
+
*
|
|
4
|
+
* Two roles:
|
|
5
|
+
* 1. Navigation rules — the most failure-prone agent behaviours are
|
|
6
|
+
* `browser_navigate` to same-origin paths (kills the widget) and
|
|
7
|
+
* reading the JS bundle for credentials. We tell the agent both
|
|
8
|
+
* mistakes by name, including the actual origin to forbid.
|
|
9
|
+
* 2. Narration format — how the widget renders the run depends on the
|
|
10
|
+
* agent emitting short imperative one-liners before each logical
|
|
11
|
+
* step. The good/bad examples are present-tense and 3–8 words.
|
|
12
|
+
*
|
|
13
|
+
* Lives in its own file because this string is the most-tuned text in the
|
|
14
|
+
* repo and the easiest to break with a typo. Tests can import directly.
|
|
15
|
+
*/
|
|
16
|
+
export function buildCdpHint(tabs) {
|
|
17
|
+
if (tabs.length === 0)
|
|
18
|
+
return '';
|
|
19
|
+
// Prefer the localhost tab if we have multiple — that's almost always the
|
|
20
|
+
// dev server the user is testing against.
|
|
21
|
+
const localhost = tabs.find(t => /localhost|127\.0\.0\.1/.test(t.url));
|
|
22
|
+
const active = localhost ?? tabs[0];
|
|
23
|
+
let activeOrigin = '';
|
|
24
|
+
try {
|
|
25
|
+
activeOrigin = new URL(active.url).origin;
|
|
26
|
+
}
|
|
27
|
+
catch { /* malformed url — fall back to no-origin guard */ }
|
|
28
|
+
return [
|
|
29
|
+
`The user's Chrome currently has these tabs open:`,
|
|
30
|
+
...tabs.map(t => ` - ${t.url}${t.title ? ` (${t.title})` : ''}`),
|
|
31
|
+
``,
|
|
32
|
+
`The likely active dev tab is: ${active.url}`,
|
|
33
|
+
``,
|
|
34
|
+
`Navigation rules — read carefully, these mistakes are the #1 cause of failed`,
|
|
35
|
+
`runs:`,
|
|
36
|
+
``,
|
|
37
|
+
` 1. Do NOT call browser_navigate to a URL that is already the active tab.`,
|
|
38
|
+
` The widget that hosts this session lives inside the page; reloading the`,
|
|
39
|
+
` page kills the WebSocket connection and your run gets aborted mid-flight.`,
|
|
40
|
+
``,
|
|
41
|
+
activeOrigin
|
|
42
|
+
? ` 2. Do NOT call browser_navigate to ANY path on origin ${activeOrigin}`
|
|
43
|
+
: ` 2. Do NOT call browser_navigate to source-file paths on the dev server`,
|
|
44
|
+
` just to "read source code for hints" — paths like /src/Login.tsx,`,
|
|
45
|
+
` /@vite/client, /node_modules/* are served by Vite as JS modules and`,
|
|
46
|
+
` loading them triggers the same widget-killing reload. To inspect the`,
|
|
47
|
+
` page, use browser_snapshot — the accessibility tree already exposes`,
|
|
48
|
+
` labels, placeholders, and roles.`,
|
|
49
|
+
``,
|
|
50
|
+
` 3. Do NOT read the JS bundle, evaluate page source, or scrape DOM for`,
|
|
51
|
+
` hardcoded credentials, API keys, or secrets. If the task needs login,`,
|
|
52
|
+
` the user must provide credentials in their prompt; if they didn't,`,
|
|
53
|
+
` report "no credentials provided" and stop — do not guess.`,
|
|
54
|
+
``,
|
|
55
|
+
` 4. To see the current page state, call browser_snapshot first. Only`,
|
|
56
|
+
` navigate if you actually need a different URL.`,
|
|
57
|
+
``,
|
|
58
|
+
`Narration format — affects how the widget renders your run for the user:`,
|
|
59
|
+
``,
|
|
60
|
+
` Before each LOGICAL STEP (a coherent unit of work like "Open the login`,
|
|
61
|
+
` form", "Fill credentials", "Verify the welcome message"), emit ONE short`,
|
|
62
|
+
` imperative sentence describing what you're about to do — present tense,`,
|
|
63
|
+
` 3–8 words, no markdown. The widget uses that sentence as the step's title.`,
|
|
64
|
+
``,
|
|
65
|
+
` Good examples:`,
|
|
66
|
+
` "Open the login form."`,
|
|
67
|
+
` "Fill credentials and submit."`,
|
|
68
|
+
` "Verify the welcome message."`,
|
|
69
|
+
` "Now testing the Counter section."`,
|
|
70
|
+
``,
|
|
71
|
+
` Bad examples (too verbose / too vague):`,
|
|
72
|
+
` "Let me check the current state of the app and then drive the login flow."`,
|
|
73
|
+
` "First, I'll take a snapshot, then I'll look at the page structure, and..."`,
|
|
74
|
+
``,
|
|
75
|
+
` After the run, if you discovered bugs or unexpected behavior, summarize`,
|
|
76
|
+
` them in the FINAL message using these markers so the widget can extract`,
|
|
77
|
+
` them into a Findings card:`,
|
|
78
|
+
``,
|
|
79
|
+
` ## Findings`,
|
|
80
|
+
` - **Bug** — <one-line summary>`,
|
|
81
|
+
` - **Minor** — <one-line summary>`,
|
|
82
|
+
``,
|
|
83
|
+
` Do NOT spread bug discoveries across mid-run narration — keep them in the`,
|
|
84
|
+
` final summary so they group cleanly. Mid-run, just narrate the next step.`,
|
|
85
|
+
].join('\n');
|
|
86
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Save-artifact WebSocket handlers (skill / spec / Jira CSV).
|
|
3
|
+
*
|
|
4
|
+
* All three save-* messages share the same shape: validate `name + steps`,
|
|
5
|
+
* call a per-kind writer, fork on Exists-error vs. success. The differences
|
|
6
|
+
* (which writer, which message names, which fields to pluck, skill's
|
|
7
|
+
* "push a fresh skills-list afterwards" tail) are captured in the
|
|
8
|
+
* `SaveArtifactConfig` descriptor table below.
|
|
9
|
+
*
|
|
10
|
+
* Replaces three near-identical handlers that drifted apart over time —
|
|
11
|
+
* see the v0.2.x refactor pass for the full rationale.
|
|
12
|
+
*/
|
|
13
|
+
import type { WebSocket } from 'ws';
|
|
14
|
+
import { writeSkill, type SkillStep } from '../skills/writeSkill.js';
|
|
15
|
+
import { writeSpec, type SpecAssertion } from '../specs/writeSpec.js';
|
|
16
|
+
import { writeCaseCsv } from '../specs/writeCaseCsv.js';
|
|
17
|
+
import { type ClientMessage } from './types.js';
|
|
18
|
+
interface SaveArtifactConfig<TWriteResult extends {
|
|
19
|
+
slug: string;
|
|
20
|
+
path: string;
|
|
21
|
+
}> {
|
|
22
|
+
/** Used in error messages. Mirrors the WS `type` the client sent. */
|
|
23
|
+
requestName: string;
|
|
24
|
+
/** Emitted on success. */
|
|
25
|
+
savedType: string;
|
|
26
|
+
/** Emitted when the writer threw an Exists-error. */
|
|
27
|
+
existsType: string;
|
|
28
|
+
/** Optional — fires after a successful write. Used by the skill flow to
|
|
29
|
+
* push a refreshed `skills-list` to the widget. */
|
|
30
|
+
onSaved?: (ws: WebSocket, devRoot: string) => Promise<void>;
|
|
31
|
+
/** Class used in `err instanceof …` to detect "already exists" errors. */
|
|
32
|
+
ExistsError: new (...args: never[]) => {
|
|
33
|
+
slug: string;
|
|
34
|
+
path: string;
|
|
35
|
+
} & Error;
|
|
36
|
+
/** Pluck the payload fields this artifact needs and call its writer. */
|
|
37
|
+
write: (args: {
|
|
38
|
+
devRoot: string;
|
|
39
|
+
name: string;
|
|
40
|
+
description: string;
|
|
41
|
+
steps: SkillStep[];
|
|
42
|
+
assertions: SpecAssertion[];
|
|
43
|
+
payload: NonNullable<ClientMessage['payload']>;
|
|
44
|
+
overwrite: boolean;
|
|
45
|
+
}) => Promise<TWriteResult>;
|
|
46
|
+
}
|
|
47
|
+
export declare function handleSaveArtifact<TWriteResult extends {
|
|
48
|
+
slug: string;
|
|
49
|
+
path: string;
|
|
50
|
+
}>(ws: WebSocket, msg: ClientMessage, devRoot: string, cfg: SaveArtifactConfig<TWriteResult>): Promise<void>;
|
|
51
|
+
export declare const SKILL_CONFIG: SaveArtifactConfig<Awaited<ReturnType<typeof writeSkill>>>;
|
|
52
|
+
export declare const SPEC_CONFIG: SaveArtifactConfig<Awaited<ReturnType<typeof writeSpec>>>;
|
|
53
|
+
export declare const CASE_CSV_CONFIG: SaveArtifactConfig<Awaited<ReturnType<typeof writeCaseCsv>>>;
|
|
54
|
+
export {};
|
|
55
|
+
//# sourceMappingURL=saveHandlers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"saveHandlers.d.ts","sourceRoot":"","sources":["../../src/service/saveHandlers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,IAAI,CAAC;AACpC,OAAO,EAAE,UAAU,EAAgC,KAAK,SAAS,EAAE,MAAM,yBAAyB,CAAC;AACnG,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;wDACoD;IACpD,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,CA+Bf;AAED,eAAO,MAAM,YAAY,EAAE,kBAAkB,CAAC,OAAO,CAAC,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC,CAanF,CAAC;AAEF,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"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Save-artifact WebSocket handlers (skill / spec / Jira CSV).
|
|
3
|
+
*
|
|
4
|
+
* All three save-* messages share the same shape: validate `name + steps`,
|
|
5
|
+
* call a per-kind writer, fork on Exists-error vs. success. The differences
|
|
6
|
+
* (which writer, which message names, which fields to pluck, skill's
|
|
7
|
+
* "push a fresh skills-list afterwards" tail) are captured in the
|
|
8
|
+
* `SaveArtifactConfig` descriptor table below.
|
|
9
|
+
*
|
|
10
|
+
* Replaces three near-identical handlers that drifted apart over time —
|
|
11
|
+
* see the v0.2.x refactor pass for the full rationale.
|
|
12
|
+
*/
|
|
13
|
+
import { writeSkill, listSkills, SkillExistsError } from '../skills/writeSkill.js';
|
|
14
|
+
import { writeSpec, SpecExistsError } from '../specs/writeSpec.js';
|
|
15
|
+
import { writeCaseCsv, CaseCsvExistsError } from '../specs/writeCaseCsv.js';
|
|
16
|
+
import { send } from './types.js';
|
|
17
|
+
export async function handleSaveArtifact(ws, msg, devRoot, cfg) {
|
|
18
|
+
const name = msg.payload?.name;
|
|
19
|
+
const description = msg.payload?.description ?? '';
|
|
20
|
+
const steps = msg.payload?.steps;
|
|
21
|
+
const assertions = msg.payload?.assertions ?? [];
|
|
22
|
+
const overwrite = msg.payload?.overwrite === true;
|
|
23
|
+
if (typeof name !== 'string' || !name.trim()) {
|
|
24
|
+
send(ws, { type: 'error', payload: { message: `${cfg.requestName}: name is required` } });
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (!Array.isArray(steps) || steps.length === 0) {
|
|
28
|
+
send(ws, { type: 'error', payload: { message: `${cfg.requestName}: no steps to save` } });
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
const result = await cfg.write({
|
|
33
|
+
devRoot, name, description, steps, assertions,
|
|
34
|
+
payload: msg.payload, overwrite,
|
|
35
|
+
});
|
|
36
|
+
send(ws, { type: cfg.savedType, payload: { name: result.slug, path: result.path } });
|
|
37
|
+
if (cfg.onSaved)
|
|
38
|
+
await cfg.onSaved(ws, devRoot);
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
if (err instanceof cfg.ExistsError) {
|
|
42
|
+
send(ws, { type: cfg.existsType, payload: { slug: err.slug, existingPath: err.path } });
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
46
|
+
send(ws, { type: 'error', payload: { message: `${cfg.requestName} failed: ${message}` } });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export const SKILL_CONFIG = {
|
|
50
|
+
requestName: 'save-skill',
|
|
51
|
+
savedType: 'skill-saved',
|
|
52
|
+
existsType: 'skill-exists',
|
|
53
|
+
ExistsError: SkillExistsError,
|
|
54
|
+
write: ({ devRoot, name, description, steps, overwrite }) => writeSkill({ devRoot, name, description, steps, overwrite }),
|
|
55
|
+
onSaved: async (ws, devRoot) => {
|
|
56
|
+
// Push a fresh list so the widget's skills overlay updates without a
|
|
57
|
+
// round-trip — most relevant right after the save.
|
|
58
|
+
const skills = await listSkills(devRoot);
|
|
59
|
+
send(ws, { type: 'skills-list', payload: { skills } });
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
export const SPEC_CONFIG = {
|
|
63
|
+
requestName: 'save-spec',
|
|
64
|
+
savedType: 'spec-saved',
|
|
65
|
+
existsType: 'spec-exists',
|
|
66
|
+
ExistsError: SpecExistsError,
|
|
67
|
+
write: ({ devRoot, name, description, steps, assertions, overwrite }) => writeSpec({ devRoot, name, description, steps, assertions, overwrite }),
|
|
68
|
+
};
|
|
69
|
+
export const CASE_CSV_CONFIG = {
|
|
70
|
+
requestName: 'save-case-csv',
|
|
71
|
+
savedType: 'case-csv-saved',
|
|
72
|
+
existsType: 'case-csv-exists',
|
|
73
|
+
ExistsError: CaseCsvExistsError,
|
|
74
|
+
write: ({ devRoot, name, description, steps, assertions, payload, overwrite }) => writeCaseCsv({
|
|
75
|
+
devRoot, name, description, steps, assertions,
|
|
76
|
+
jiraProjectKey: payload.jiraProjectKey,
|
|
77
|
+
labels: payload.labels,
|
|
78
|
+
overwrite,
|
|
79
|
+
}),
|
|
80
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared types for the service/ handler modules.
|
|
3
|
+
*
|
|
4
|
+
* `ClientMessage` describes the wire-protocol envelope every message from
|
|
5
|
+
* the widget arrives in. Lives here (not in service.ts) so individual
|
|
6
|
+
* handlers can type their `msg` argument without circular imports.
|
|
7
|
+
*
|
|
8
|
+
* `send` is the one-liner used by every handler to emit a typed message
|
|
9
|
+
* back to the widget. Centralised so the JSON.stringify happens in exactly
|
|
10
|
+
* one place.
|
|
11
|
+
*/
|
|
12
|
+
import type { WebSocket } from 'ws';
|
|
13
|
+
import type { SkillStep } from '../skills/writeSkill.js';
|
|
14
|
+
import type { SpecAssertion } from '../specs/writeSpec.js';
|
|
15
|
+
export interface ClientMessage {
|
|
16
|
+
type: string;
|
|
17
|
+
payload?: {
|
|
18
|
+
text?: string;
|
|
19
|
+
sessionId?: string;
|
|
20
|
+
name?: string;
|
|
21
|
+
description?: string;
|
|
22
|
+
steps?: SkillStep[];
|
|
23
|
+
assertions?: SpecAssertion[];
|
|
24
|
+
overwrite?: boolean;
|
|
25
|
+
/** save-case-csv only — passed through to writeCaseCsv as extra
|
|
26
|
+
* fields on the test case's Labels column. */
|
|
27
|
+
jiraProjectKey?: string;
|
|
28
|
+
labels?: string;
|
|
29
|
+
/** check-cdp / launch-chrome / focus-debug — the widget's
|
|
30
|
+
* window.location.href so service can compare origins or navigate the
|
|
31
|
+
* newly-launched debug Chrome to the same URL. */
|
|
32
|
+
pageUrl?: string;
|
|
33
|
+
/** switch-agent only — id of the agent to switch the service to. */
|
|
34
|
+
agentId?: string;
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
export declare function send(ws: WebSocket, message: {
|
|
38
|
+
type: string;
|
|
39
|
+
payload?: unknown;
|
|
40
|
+
}): void;
|
|
41
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/service/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,IAAI,CAAC;AACpC,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;KAClB,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"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared types for the service/ handler modules.
|
|
3
|
+
*
|
|
4
|
+
* `ClientMessage` describes the wire-protocol envelope every message from
|
|
5
|
+
* the widget arrives in. Lives here (not in service.ts) so individual
|
|
6
|
+
* handlers can type their `msg` argument without circular imports.
|
|
7
|
+
*
|
|
8
|
+
* `send` is the one-liner used by every handler to emit a typed message
|
|
9
|
+
* back to the widget. Centralised so the JSON.stringify happens in exactly
|
|
10
|
+
* one place.
|
|
11
|
+
*/
|
|
12
|
+
export function send(ws, message) {
|
|
13
|
+
ws.send(JSON.stringify(message));
|
|
14
|
+
}
|
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":"AAmEA,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;CAClB;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,CAmT/E"}
|