qiksy-mcp 1.41.0 → 1.43.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 +28 -1
- package/connect.mjs +203 -0
- package/package.json +2 -1
- package/server.mjs +106 -10
package/README.md
CHANGED
|
@@ -22,7 +22,7 @@ unless the licence (or the 28-day trial) is valid, so once a trial lapses *every
|
|
|
22
22
|
tool fails — `qa_export` included — with a message saying so. Do not plan a
|
|
23
23
|
free-tier integration around read-only tools.
|
|
24
24
|
|
|
25
|
-
**npm carries `qiksy-mcp@1.
|
|
25
|
+
**npm carries `qiksy-mcp@1.43.0`** — the current build, with all sixty-two tools
|
|
26
26
|
including a filterable full-page `qa_snapshot`, `qa_fill_json` (a whole wizard step —
|
|
27
27
|
custom dropdowns, calendars, cascaders, trees, tags and time included — in one call),
|
|
28
28
|
the `qa_pick_*` verbs for the composites, `qa_probe` for the one control that refuses,
|
|
@@ -241,6 +241,33 @@ Qiksy runs several logins on one domain at once — one per tab, each with its o
|
|
|
241
241
|
|
|
242
242
|
## Setup
|
|
243
243
|
|
|
244
|
+
### The short way: one command
|
|
245
|
+
|
|
246
|
+
```bash
|
|
247
|
+
npx qiksy-mcp connect
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
Run it **in the project you work in**. It picks a free port, mints a token, writes the `qiksy`
|
|
251
|
+
server into every agent config it finds on the machine — Claude Code (`.mcp.json`), Cursor
|
|
252
|
+
(`~/.cursor/mcp.json`), VS Code (`.vscode/mcp.json`, which uses its own `servers` key), Windsurf,
|
|
253
|
+
Claude Desktop — and puts the token on your clipboard. Then: **popup → Agent bridge → On → paste**,
|
|
254
|
+
restart the agent, ask it to call `qa_status`.
|
|
255
|
+
|
|
256
|
+
`npx qiksy-mcp init` does the same **and** writes the agent guide into the project, which is what
|
|
257
|
+
stops your agent proposing Playwright on the first unfamiliar field. Use `init` the first time.
|
|
258
|
+
|
|
259
|
+
What it will not do: it writes a config **only where that client's own folder already exists**, so
|
|
260
|
+
you never get a Windsurf config for an editor you do not have; it keeps every other server already
|
|
261
|
+
in those files; it refuses to touch a config it cannot parse rather than overwriting it; and on a
|
|
262
|
+
second run it **keeps the token that is already there**, so a browser you have already paired stays
|
|
263
|
+
paired. On Windows the entry is written as `cmd /c npx …`, which is the form that actually starts —
|
|
264
|
+
a bare `npx` fails in about half the clients there.
|
|
265
|
+
|
|
266
|
+
Flags: `--dir <path>` (which project), `--port <n>`, `--token <value>`, and `--no-connect` on
|
|
267
|
+
`init` if you want the guide without the wiring.
|
|
268
|
+
|
|
269
|
+
### The manual way
|
|
270
|
+
|
|
244
271
|
1. **In the extension popup** → *Agent bridge*: toggle **On**, click **Generate** for a token, keep the default port `7333` (or pick your own). It **saves as you go** — there is no separate Save step.
|
|
245
272
|
2. **Click 📋 Copy agent config** in the popup — it copies the whole `mcpServers` block below with **your port and token already filled in** — and paste it into your agent's MCP config file (below). No hand-editing the token or port.
|
|
246
273
|
3. **Watch the status line** under the toggle. 🟡 *Waiting…* until your agent's `qiksy-mcp` is up, then 🟢 *Agent connected on port 7333*. If it stays yellow or turns red, see [Connection status & troubleshooting](#connection-status--troubleshooting).
|
package/connect.mjs
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `npx qiksy-mcp connect` — THE SIX STEPS BECOME ONE.
|
|
3
|
+
*
|
|
4
|
+
* Owner, 20.09.2026: «QA ленивые, технически многие вообще не шарят — как к блютузу бы».
|
|
5
|
+
* Measured against the old path, a person had to: switch the bridge on, accept, press Generate,
|
|
6
|
+
* press Copy, FIND their agent's config file, paste JSON into it without breaking the JSON, and
|
|
7
|
+
* restart the agent. Two of those steps are ours and five are theirs — and the file one is where
|
|
8
|
+
* they stop, because «~/.cursor/mcp.json» means nothing to somebody who tests web shops for a
|
|
9
|
+
* living.
|
|
10
|
+
*
|
|
11
|
+
* This process is the one thing that already lives on their machine AND is allowed to write
|
|
12
|
+
* files, so it does the whole middle: picks a free port, mints the token, finds every agent that
|
|
13
|
+
* is actually installed, writes the entry into each config it found, and leaves the token on the
|
|
14
|
+
* clipboard so the popup is one ⌘V away. The extension cannot do any of that — no filesystem, no
|
|
15
|
+
* processes — which is why this belongs here and not in the popup.
|
|
16
|
+
*
|
|
17
|
+
* Three rules it does not break:
|
|
18
|
+
* · nothing is invented. A config is written ONLY where that client's own folder already
|
|
19
|
+
* exists, so nobody ends up with a Windsurf config for an editor they never installed.
|
|
20
|
+
* · nothing of theirs is lost. The file is read, our one entry is set, everything else is
|
|
21
|
+
* written back untouched — and a run on a file we cannot parse stops rather than overwrites.
|
|
22
|
+
* · the pairing survives. A second run keeps the token that is already in their config, or the
|
|
23
|
+
* browser they already paired would be locked out by a fresh secret.
|
|
24
|
+
*/
|
|
25
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
26
|
+
import { homedir, platform as osPlatform } from 'node:os';
|
|
27
|
+
import { join } from 'node:path';
|
|
28
|
+
import { createServer } from 'node:net';
|
|
29
|
+
import { spawnSync } from 'node:child_process';
|
|
30
|
+
import { randomBytes } from 'node:crypto';
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* WHERE EACH AGENT KEEPS ITS SERVER LIST.
|
|
34
|
+
*
|
|
35
|
+
* `home` and `platform` are arguments rather than reads so a test can ask for the Windows answer
|
|
36
|
+
* on a Mac — the paths are the part that goes wrong silently, and they are unreachable otherwise.
|
|
37
|
+
*
|
|
38
|
+
* `key` differs by client and is NOT cosmetic: VS Code reads `servers`, everyone else reads
|
|
39
|
+
* `mcpServers`, and a file written with the wrong key is valid JSON that the agent ignores — the
|
|
40
|
+
* worst possible failure, because it looks installed.
|
|
41
|
+
*
|
|
42
|
+
* `needs` is the folder whose existence means the client is installed. Missing → skipped, which
|
|
43
|
+
* is how «wrote 4 configs» stops being a lie on a machine with one editor.
|
|
44
|
+
*/
|
|
45
|
+
export function agentTargets({ home = homedir(), platform = osPlatform(), dir = process.cwd() } = {}) {
|
|
46
|
+
const appData = process.env.APPDATA || join(home, 'AppData', 'Roaming');
|
|
47
|
+
const claudeDesktop =
|
|
48
|
+
platform === 'win32'
|
|
49
|
+
? join(appData, 'Claude', 'claude_desktop_config.json')
|
|
50
|
+
: platform === 'darwin'
|
|
51
|
+
? join(home, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json')
|
|
52
|
+
: join(home, '.config', 'Claude', 'claude_desktop_config.json');
|
|
53
|
+
|
|
54
|
+
return [
|
|
55
|
+
/* Claude Code reads a project-scoped .mcp.json — the file belongs to the repo the person is
|
|
56
|
+
standing in, which is also the repo their agent is working on. */
|
|
57
|
+
{ id: 'claude-code', label: 'Claude Code', file: join(dir, '.mcp.json'), key: 'mcpServers', needs: dir, scope: 'project' },
|
|
58
|
+
{ id: 'cursor', label: 'Cursor', file: join(home, '.cursor', 'mcp.json'), key: 'mcpServers', needs: join(home, '.cursor'), scope: 'user' },
|
|
59
|
+
{ id: 'vscode', label: 'VS Code', file: join(dir, '.vscode', 'mcp.json'), key: 'servers', needs: join(dir, '.vscode'), scope: 'project' },
|
|
60
|
+
{ id: 'windsurf', label: 'Windsurf', file: join(home, '.codeium', 'windsurf', 'mcp_config.json'), key: 'mcpServers', needs: join(home, '.codeium', 'windsurf'), scope: 'user' },
|
|
61
|
+
{ id: 'claude-desktop', label: 'Claude Desktop', file: claudeDesktop, key: 'mcpServers', needs: join(claudeDesktop, '..'), scope: 'user' },
|
|
62
|
+
];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* THE ENTRY ITSELF — and on Windows it is not the obvious one.
|
|
67
|
+
*
|
|
68
|
+
* `command: "npx"` is what every example shows and what fails on Windows for a good half of the
|
|
69
|
+
* clients: `npx` there is `npx.cmd`, and a spawn without a shell does not find it. The result is
|
|
70
|
+
* an agent that reports the server as failed to start with no explanation a tester can act on.
|
|
71
|
+
* So Windows gets the `cmd /c` form, which works in both the clients that need it and those that
|
|
72
|
+
* would have coped anyway.
|
|
73
|
+
*/
|
|
74
|
+
export function serverEntry({ port, token, platform = osPlatform() }) {
|
|
75
|
+
const base = platform === 'win32'
|
|
76
|
+
? { command: 'cmd', args: ['/c', 'npx', 'qiksy-mcp', '--port', String(port)] }
|
|
77
|
+
: { command: 'npx', args: ['qiksy-mcp', '--port', String(port)] };
|
|
78
|
+
return { ...base, env: { QIKSY_MCP_TOKEN: token } };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Read a config we are about to edit. Unreadable JSON is an error, never something to overwrite. */
|
|
82
|
+
export function readConfig(file) {
|
|
83
|
+
if (!existsSync(file)) return { json: {}, existed: false };
|
|
84
|
+
const raw = readFileSync(file, 'utf8').trim();
|
|
85
|
+
if (!raw) return { json: {}, existed: true };
|
|
86
|
+
try {
|
|
87
|
+
return { json: JSON.parse(raw), existed: true };
|
|
88
|
+
} catch (e) {
|
|
89
|
+
throw new Error(`${file} is not valid JSON (${e.message}) — fix or move it, I will not overwrite it`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** The token already wired into one of their configs, if any: a second run must not break the pair. */
|
|
94
|
+
export function tokenInPlace(targets) {
|
|
95
|
+
for (const t of targets) {
|
|
96
|
+
try {
|
|
97
|
+
if (!existsSync(t.file)) continue;
|
|
98
|
+
const { json } = readConfig(t.file);
|
|
99
|
+
const tok = json?.[t.key]?.qiksy?.env?.QIKSY_MCP_TOKEN;
|
|
100
|
+
if (typeof tok === 'string' && tok.length >= 16) return tok;
|
|
101
|
+
} catch {
|
|
102
|
+
/* a broken file is reported later by the write; it must not stop the search */
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return '';
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** The port already wired in, for the same reason: re-running must not move the browser's target. */
|
|
109
|
+
export function portInPlace(targets) {
|
|
110
|
+
for (const t of targets) {
|
|
111
|
+
try {
|
|
112
|
+
if (!existsSync(t.file)) continue;
|
|
113
|
+
const { json } = readConfig(t.file);
|
|
114
|
+
const args = json?.[t.key]?.qiksy?.args;
|
|
115
|
+
const i = Array.isArray(args) ? args.indexOf('--port') : -1;
|
|
116
|
+
const p = i !== -1 ? Number(args[i + 1]) : NaN;
|
|
117
|
+
if (Number.isInteger(p) && p > 0) return p;
|
|
118
|
+
} catch {
|
|
119
|
+
/* same */
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return 0;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Write our one entry, keep everything else in the file exactly as it was. */
|
|
126
|
+
export function writeEntry(target, entry) {
|
|
127
|
+
const { json, existed } = readConfig(target.file);
|
|
128
|
+
const next = { ...json, [target.key]: { ...(json[target.key] || {}), qiksy: entry } };
|
|
129
|
+
mkdirSync(join(target.file, '..'), { recursive: true });
|
|
130
|
+
writeFileSync(target.file, `${JSON.stringify(next, null, 2)}\n`, 'utf8');
|
|
131
|
+
return existed ? 'updated' : 'created';
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** A port nothing is listening on, starting where the docs say and walking up a short way. */
|
|
135
|
+
export async function freePort(from = 7333, tries = 12) {
|
|
136
|
+
for (let p = from; p < from + tries; p++) {
|
|
137
|
+
const ok = await new Promise((res) => {
|
|
138
|
+
const s = createServer();
|
|
139
|
+
s.once('error', () => res(false));
|
|
140
|
+
s.once('listening', () => s.close(() => res(true)));
|
|
141
|
+
s.listen(p, '127.0.0.1');
|
|
142
|
+
});
|
|
143
|
+
if (ok) return p;
|
|
144
|
+
}
|
|
145
|
+
return from;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* THE TOKEN GOES TO THE CLIPBOARD, because the last step is a paste into the popup.
|
|
150
|
+
* Best effort by design: a headless Linux box has no clipboard, and that is not a failure of the
|
|
151
|
+
* setup — the token is printed as well, and the report says which of the two happened.
|
|
152
|
+
*/
|
|
153
|
+
export function toClipboard(text, platform = osPlatform()) {
|
|
154
|
+
const runners = platform === 'darwin'
|
|
155
|
+
? [['pbcopy', []]]
|
|
156
|
+
: platform === 'win32'
|
|
157
|
+
? [['clip', []]]
|
|
158
|
+
: [['wl-copy', []], ['xclip', ['-selection', 'clipboard']], ['xsel', ['--clipboard', '--input']]];
|
|
159
|
+
for (const [cmd, args] of runners) {
|
|
160
|
+
try {
|
|
161
|
+
const r = spawnSync(cmd, args, { input: text, shell: platform === 'win32' });
|
|
162
|
+
if (!r.error && r.status === 0) return cmd;
|
|
163
|
+
} catch {
|
|
164
|
+
/* try the next one */
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return '';
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export const mintToken = () => randomBytes(18).toString('base64url');
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Do the whole thing and report what happened. No printing here — the caller owns the voice, and
|
|
174
|
+
* a function that both decides and narrates cannot be tested without reading its output.
|
|
175
|
+
*/
|
|
176
|
+
export async function connectAgents({ dir = process.cwd(), home = homedir(), platform = osPlatform(), port, token } = {}) {
|
|
177
|
+
const targets = agentTargets({ home, platform, dir });
|
|
178
|
+
const installed = targets.filter((t) => existsSync(t.needs));
|
|
179
|
+
const keptToken = token || tokenInPlace(targets);
|
|
180
|
+
const keptPort = port || portInPlace(targets);
|
|
181
|
+
const usePort = keptPort || (await freePort());
|
|
182
|
+
const useToken = keptToken || mintToken();
|
|
183
|
+
const entry = serverEntry({ port: usePort, token: useToken, platform });
|
|
184
|
+
|
|
185
|
+
const wrote = [];
|
|
186
|
+
const failed = [];
|
|
187
|
+
for (const t of installed) {
|
|
188
|
+
try {
|
|
189
|
+
wrote.push({ ...t, action: writeEntry(t, entry) });
|
|
190
|
+
} catch (e) {
|
|
191
|
+
failed.push({ ...t, error: e.message });
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return {
|
|
195
|
+
port: usePort,
|
|
196
|
+
token: useToken,
|
|
197
|
+
reusedToken: !!keptToken,
|
|
198
|
+
entry,
|
|
199
|
+
wrote,
|
|
200
|
+
failed,
|
|
201
|
+
skipped: targets.filter((t) => !installed.includes(t)),
|
|
202
|
+
};
|
|
203
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "qiksy-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.43.0",
|
|
4
4
|
"description": "Browser MCP server for the Chrome tab you already have open — your session, your logins. Gives Claude Code, Cursor, Codex and VS Code the live page: findings, forms, failed requests with server bodies.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mcp",
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
"gaps.mjs",
|
|
36
36
|
"gallery.mjs",
|
|
37
37
|
"svg-shot.mjs",
|
|
38
|
+
"connect.mjs",
|
|
38
39
|
"diff.mjs",
|
|
39
40
|
"README.md",
|
|
40
41
|
"AGENT-GUIDE.md"
|
package/server.mjs
CHANGED
|
@@ -35,6 +35,7 @@ import { project } from './project.mjs';
|
|
|
35
35
|
import { recordGap, listGaps, gapInvite, GAPS_FILE } from './gaps.mjs';
|
|
36
36
|
import { STATES, STATE_KEYS, renderGallery } from './gallery.mjs';
|
|
37
37
|
import { renderSearchableSvg } from './svg-shot.mjs';
|
|
38
|
+
import { connectAgents, toClipboard } from './connect.mjs';
|
|
38
39
|
|
|
39
40
|
const log = (...a) => console.error('[qiksy-mcp]', ...a);
|
|
40
41
|
|
|
@@ -669,7 +670,7 @@ function installDocs({ dir, out, withClaude = true }) {
|
|
|
669
670
|
return report;
|
|
670
671
|
}
|
|
671
672
|
|
|
672
|
-
function runInit(argv) {
|
|
673
|
+
async function runInit(argv) {
|
|
673
674
|
const flag = (name) => {
|
|
674
675
|
const i = argv.indexOf(name);
|
|
675
676
|
return i !== -1 ? argv[i + 1] : undefined;
|
|
@@ -695,15 +696,60 @@ function runInit(argv) {
|
|
|
695
696
|
for (const t of report.touched) say(`✓ ${t.action} the Qiksy block in ${t.file}`);
|
|
696
697
|
for (const f of report.failed) console.error(` could not write ${f.file} — ${f.error}`);
|
|
697
698
|
|
|
699
|
+
if (!argv.includes('--no-connect')) await runConnect(argv, { after: true });
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
/**
|
|
703
|
+
* THE WIRING, DONE BY THE THING THAT IS ALLOWED TO WRITE FILES.
|
|
704
|
+
*
|
|
705
|
+
* What used to be printed here was three steps in a terminal and a JSON file to edit by hand, and
|
|
706
|
+
* that file is where people stop (owner, 20.09.2026: «технически многие вообще не шарят»). The
|
|
707
|
+
* extension cannot do it — no filesystem, no processes — and the person should not have to, so
|
|
708
|
+
* this process does: free port, token, every installed agent's config written, token on the
|
|
709
|
+
* clipboard for the one paste that is left.
|
|
710
|
+
*
|
|
711
|
+
* It says what it TOUCHED and what it skipped, because a setup tool that writes into somebody's
|
|
712
|
+
* machine and reports only success is the kind you uninstall.
|
|
713
|
+
*/
|
|
714
|
+
async function runConnect(argv, { after = false } = {}) {
|
|
715
|
+
const flag = (name) => {
|
|
716
|
+
const i = argv.indexOf(name);
|
|
717
|
+
return i !== -1 ? argv[i + 1] : undefined;
|
|
718
|
+
};
|
|
719
|
+
const say = (...a) => console.log(...a);
|
|
720
|
+
let r;
|
|
721
|
+
try {
|
|
722
|
+
r = await connectAgents({
|
|
723
|
+
dir: resolve(flag('--dir') || process.cwd()),
|
|
724
|
+
port: Number(flag('--port')) || 0,
|
|
725
|
+
token: flag('--token') || '',
|
|
726
|
+
});
|
|
727
|
+
} catch (e) {
|
|
728
|
+
console.error(`qiksy-mcp connect: ${e.message}`);
|
|
729
|
+
process.exit(1);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
say(after ? '' : '');
|
|
733
|
+
if (!r.wrote.length) {
|
|
734
|
+
say('No agent config found to write into. Looked for:');
|
|
735
|
+
for (const s of r.skipped) say(` · ${s.label} → ${s.file}`);
|
|
736
|
+
say('\nOpen the project you work in and run this there, or paste the block from');
|
|
737
|
+
say('https://qiksy.app/qa-copilot/ into your agent yourself.');
|
|
738
|
+
}
|
|
739
|
+
for (const w of r.wrote) say(`✓ ${w.action} the qiksy server in ${w.label} → ${w.file}`);
|
|
740
|
+
for (const f of r.failed) console.error(` could not write ${f.label} → ${f.file}: ${f.error}`);
|
|
741
|
+
|
|
742
|
+
const clip = toClipboard(r.token);
|
|
698
743
|
say(`
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
744
|
+
Port ${r.port}${r.reusedToken ? ' · kept the token already in your config' : ' · new token'}.
|
|
745
|
+
|
|
746
|
+
One step left, in the browser:
|
|
747
|
+
Qiksy popup → "Agent bridge · MCP" → On → paste the token${clip ? ' (it is on your clipboard)' : ''}.${clip ? '' : `\n Token: ${r.token}`}
|
|
748
|
+
Then restart your agent and ask it to call qa_status.
|
|
749
|
+
|
|
750
|
+
The token never leaves this machine: it is what stops another local program from pretending to be
|
|
751
|
+
your browser. "Qiksy extension is not connected" means the popup toggle is off or the token and
|
|
752
|
+
port differ — it never means you should reach for another browser driver.`);
|
|
707
753
|
}
|
|
708
754
|
|
|
709
755
|
/**
|
|
@@ -763,8 +809,20 @@ function autoInstallDocs() {
|
|
|
763
809
|
}
|
|
764
810
|
}
|
|
765
811
|
|
|
812
|
+
/* BOTH SUBCOMMANDS END THE PROCESS, AND THE AWAIT IS THE POINT.
|
|
813
|
+
*
|
|
814
|
+
* Everything below this line boots a stdio MCP server. The first cut fired the async connect and
|
|
815
|
+
* carried on, so `connect` wired the configs AND announced «MCP stdio server ready» — a setup
|
|
816
|
+
* command turning itself into a bridge nobody asked for (seen in the first live run). Top-level
|
|
817
|
+
* await holds the module here until the work is done, and then the process leaves. */
|
|
766
818
|
if (process.argv[2] === 'init') {
|
|
767
|
-
runInit(process.argv.slice(3));
|
|
819
|
+
await runInit(process.argv.slice(3));
|
|
820
|
+
process.exit(0);
|
|
821
|
+
}
|
|
822
|
+
if (process.argv[2] === 'connect') {
|
|
823
|
+
/* The wiring on its own, for somebody who already has the guide and just wants the link up
|
|
824
|
+
(or is redoing it after changing editors). `init` runs both. */
|
|
825
|
+
await runConnect(process.argv.slice(3));
|
|
768
826
|
process.exit(0);
|
|
769
827
|
}
|
|
770
828
|
|
|
@@ -5808,6 +5866,44 @@ server.registerTool(
|
|
|
5808
5866
|
},
|
|
5809
5867
|
);
|
|
5810
5868
|
|
|
5869
|
+
/**
|
|
5870
|
+
* HOVER — the state every other verb here passes through and none of them stays in.
|
|
5871
|
+
*
|
|
5872
|
+
* Asked for by the owner on 17.09.2026 while testing with the bridge, and the gap was real: the
|
|
5873
|
+
* only verb that sent a `mouseover` was `qa_click`, which sends a `mousedown` a millisecond
|
|
5874
|
+
* later. So a menu that opens on hover was opened and closed by the same call, and a tooltip
|
|
5875
|
+
* could not be read at all. Hover is not a weaker click — it is a different state, and a large
|
|
5876
|
+
* part of what a tester checks lives only inside it.
|
|
5877
|
+
*/
|
|
5878
|
+
server.registerTool(
|
|
5879
|
+
'qa_hover',
|
|
5880
|
+
{
|
|
5881
|
+
title: 'Hover over an element and hold (Pro)',
|
|
5882
|
+
description:
|
|
5883
|
+
'Put the pointer on an element AND LEAVE IT THERE, then report what that revealed: a menu that opens on hover, a tooltip, the actions a row only shows under the pointer, the value a chart reads out beneath it. qa_click cannot answer any of those — it presses a millisecond after it hovers, and the press closes what the hover opened. ' +
|
|
5884
|
+
'It holds for `holdMs` (500 by default) before measuring, because tooltips wait — measuring at once reports "no tooltip" about one that was always going to appear. The reply carries the usual `changed` diff plus `tooltip` (role=tooltip, aria-describedby or title, whichever exists) and `expanded` when the control changed its aria-expanded. ' +
|
|
5885
|
+
'ONE FACT TO READ CAREFULLY: `cssHover` says whether the browser\'s own `:hover` matched. Dispatched events run the page\'s JavaScript handlers — every menu and tooltip library — but they do not move the real cursor, so an effect written PURELY in CSS does not appear and `cssHover` comes back false. That is the tool being honest, not the page being broken. With trusted input switched on the real pointer moves, `cssHover` is true, and CSS-only hovers light up too. ' +
|
|
5886
|
+
'Call it again with `off: true` to take the pointer away — "the tooltip disappears" is the other half of the check, and it needs its own call.' +
|
|
5887
|
+
DRIVE_NOTE,
|
|
5888
|
+
inputSchema: {
|
|
5889
|
+
tabId: tabIdArg,
|
|
5890
|
+
selector: z.string().optional().describe('CSS selector of the element to hover'),
|
|
5891
|
+
ref: z.string().optional().describe('A ref from qa_snapshot (e.g. "e12") — preferred over selector'),
|
|
5892
|
+
x: z.number().optional().describe('Without ref/selector: CSS pixels from the LEFT of the viewport. WITH one: an offset inside the element, for a chart or a split control where the middle is the wrong place.'),
|
|
5893
|
+
y: z.number().optional().describe('Without ref/selector: CSS pixels from the TOP of the viewport. WITH one: an offset inside the element.'),
|
|
5894
|
+
holdMs: z.number().optional().describe('How long to hold the pointer there before measuring (0–10000, default 500)'),
|
|
5895
|
+
off: z.boolean().optional().describe('Take the pointer AWAY from whatever it is on, and report what was left. No target needed.'),
|
|
5896
|
+
},
|
|
5897
|
+
},
|
|
5898
|
+
async ({ tabId, selector, ref, x, y, holdMs, off }) => {
|
|
5899
|
+
try {
|
|
5900
|
+
return asText(await callExtension('qa_hover', { tabId, selector, ref, x, y, holdMs, off }, 30_000));
|
|
5901
|
+
} catch (e) {
|
|
5902
|
+
return asError(e);
|
|
5903
|
+
}
|
|
5904
|
+
},
|
|
5905
|
+
);
|
|
5906
|
+
|
|
5811
5907
|
server.registerTool(
|
|
5812
5908
|
'qa_click',
|
|
5813
5909
|
{
|