qiksy-mcp 1.43.0 → 1.45.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 +1 -1
- package/package.json +1 -1
- package/server.mjs +226 -13
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.44.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,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "qiksy-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.45.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",
|
package/server.mjs
CHANGED
|
@@ -825,6 +825,48 @@ if (process.argv[2] === 'connect') {
|
|
|
825
825
|
await runConnect(process.argv.slice(3));
|
|
826
826
|
process.exit(0);
|
|
827
827
|
}
|
|
828
|
+
if (process.argv[2] === 'done') {
|
|
829
|
+
/**
|
|
830
|
+
* «THE RUN IS OVER» FROM OUTSIDE THE MODEL — for any agent whose client can run a command when
|
|
831
|
+
* it finishes a turn (Claude Code's Stop hook, and the equivalent in the others).
|
|
832
|
+
*
|
|
833
|
+
* The verb `qa_done` is the ordinary route and every MCP client is told about it in the
|
|
834
|
+
* instructions. This is the route that does not depend on a model remembering: a hook fires
|
|
835
|
+
* whether or not the agent thought to say anything, so the person in the next room hears the
|
|
836
|
+
* end of the run either way.
|
|
837
|
+
*
|
|
838
|
+
* npx qiksy-mcp done → the run ended well
|
|
839
|
+
* npx qiksy-mcp done --problem → it ended badly
|
|
840
|
+
*
|
|
841
|
+
* It joins the bridge the SAME WAY a sibling agent window does, as a follower of whoever owns
|
|
842
|
+
* the port — so it never takes the port, never disturbs a running session, and costs one
|
|
843
|
+
* socket for under a second. If nothing is listening it says so and exits 0: a hook that fails
|
|
844
|
+
* loudly because the browser is closed would be a hook people remove.
|
|
845
|
+
*/
|
|
846
|
+
const bad = process.argv.includes('--problem');
|
|
847
|
+
const said = await new Promise((res) => {
|
|
848
|
+
let ws;
|
|
849
|
+
const give = (msg) => { try { ws?.close(); } catch { /* already gone */ } res(msg); };
|
|
850
|
+
const timer = setTimeout(() => give('no answer from the bridge'), 6000);
|
|
851
|
+
try {
|
|
852
|
+
ws = new WebSocket(`ws://${HOST}:${PORT}`);
|
|
853
|
+
} catch {
|
|
854
|
+
clearTimeout(timer);
|
|
855
|
+
return res('could not reach the bridge');
|
|
856
|
+
}
|
|
857
|
+
ws.on('open', () => ws.send(JSON.stringify({ t: 'join', v: PROTOCOL_VERSION, token: TOKEN, pid: process.pid })));
|
|
858
|
+
ws.on('message', (raw) => {
|
|
859
|
+
let m;
|
|
860
|
+
try { m = JSON.parse(raw.toString()); } catch { return; }
|
|
861
|
+
if (m.t === 'welcome') return ws.send(JSON.stringify({ t: 'req', id: 'done', tool: 'qa_done', args: { verdict: bad ? 'problem' : 'ok' }, timeoutMs: 5000 }));
|
|
862
|
+
if (m.t === 'res') { clearTimeout(timer); give(m.result?.soundNote || m.error || 'done'); }
|
|
863
|
+
});
|
|
864
|
+
ws.on('error', () => { clearTimeout(timer); give('no bridge on this port'); });
|
|
865
|
+
ws.on('close', () => { clearTimeout(timer); give('the bridge closed the connection'); });
|
|
866
|
+
});
|
|
867
|
+
log(said);
|
|
868
|
+
process.exit(0);
|
|
869
|
+
}
|
|
828
870
|
|
|
829
871
|
if (!TOKEN) {
|
|
830
872
|
log('WARNING: QIKSY_MCP_TOKEN is not set. Set the same token here and in the Qiksy popup, or connections are refused.');
|
|
@@ -4344,6 +4386,16 @@ that rides with it: re-say it in that language rather than translating it word f
|
|
|
4344
4386
|
the tone it names. It is rate-limited, so it will not become a tic. QIKSY_MCP_VOICE=nice softens
|
|
4345
4387
|
it, =off removes it.
|
|
4346
4388
|
|
|
4389
|
+
SAY WHEN YOU HAVE FINISHED — qa_done, one call, at the end of the run. The person who set you off
|
|
4390
|
+
is usually not watching: they are in another window, another screen, another room. That call makes
|
|
4391
|
+
the mark on every driven tab they are NOT looking at pulse slowly, and plays a short sound, so the
|
|
4392
|
+
end of the work reaches them where they actually are. It stops by itself the moment they look at
|
|
4393
|
+
one of those tabs; there is nothing for them to dismiss.
|
|
4394
|
+
It is the END, not a progress beat. A full stop after nothing happened is refused and says so, and
|
|
4395
|
+
a second one within half a minute stays silent — both on purpose, because a signal that cries wolf
|
|
4396
|
+
is one people switch off in a week. Call it once, when the work is actually over, and still write
|
|
4397
|
+
what you did in your own words: this is a nudge towards the screen, never the report itself.
|
|
4398
|
+
|
|
4347
4399
|
Two failures are NOT yours to fix, and each names its own cause: "extension is not connected"
|
|
4348
4400
|
means the MCP bridge toggle in the popup is off (or its token/port differ) — only the user can
|
|
4349
4401
|
turn it on. "Port … is held by a process that is not a Qiksy bridge" means something else owns
|
|
@@ -4601,7 +4653,9 @@ server.registerTool(
|
|
|
4601
4653
|
description:
|
|
4602
4654
|
'ADD MARKS TO A SHOT ON THE SHELF. They are stored beside the clean image, never burned into it, so the tester can move them, resize them and delete them afterwards — and so you can read them back later. ' +
|
|
4603
4655
|
'Coordinates are in IMAGE pixels; qa_screenshot returns a map of the page in exactly those pixels, so a box from the map can be passed straight through. ' +
|
|
4604
|
-
'Shapes: `box` round a region, `arrow` from one point to another, `text` for a caption with its own words. Weight is `k`; colour is one of the five the panel offers. ' +
|
|
4656
|
+
'Shapes: `box` round a region, `arrow` from one point to another, `text` for a caption with its own words, `blur` for a pane that makes what is under it unreadable. Weight is `k`; colour is one of the five the panel offers. ' +
|
|
4657
|
+
'A caption carries an optional `to` — the point it is ABOUT — and a leader line is then drawn from the plate to that point, in both the editor and the exported file. Give a box a caption with `to` set to the middle of that box and you get the callout a person would draw by hand; the tester can then drag the words anywhere and the line follows. ' +
|
|
4658
|
+
'A `blur` pane hides an address, a customer name or a staging token. It is applied when the picture is EXPORTED, copied or built into a report — the clean shot stays clean on the shelf, which is what lets the pane be moved and removed afterwards. ' +
|
|
4605
4659
|
'Adds by default; pass `replace: true` to put your marks in place of everything on the picture — which is the destructive one, and it is opt-in for that reason. ' +
|
|
4606
4660
|
'The reply is the STATE — how many marks the picture now carries and all of them in full — rather than the word «done».',
|
|
4607
4661
|
inputSchema: {
|
|
@@ -4627,6 +4681,15 @@ server.registerTool(
|
|
|
4627
4681
|
x: z.number(), y: z.number(),
|
|
4628
4682
|
s: z.string().describe('The caption itself'),
|
|
4629
4683
|
c: z.string(), k: z.number().optional().describe('Type size, 8–96'),
|
|
4684
|
+
to: z
|
|
4685
|
+
.object({ x: z.number(), y: z.number() })
|
|
4686
|
+
.optional()
|
|
4687
|
+
.describe('The point this caption is about — a leader line is drawn from the plate to it, and it follows the words if the tester moves them'),
|
|
4688
|
+
}),
|
|
4689
|
+
z.object({
|
|
4690
|
+
t: z.literal('blur'),
|
|
4691
|
+
x: z.number(), y: z.number(), w: z.number(), h: z.number(),
|
|
4692
|
+
k: z.number().optional().describe('How hard it hides, 4–60 (default 14). No colour: a pane is not a remark about the page, it is a piece of the page taken away.'),
|
|
4630
4693
|
}),
|
|
4631
4694
|
]),
|
|
4632
4695
|
)
|
|
@@ -4731,19 +4794,30 @@ server.registerTool(
|
|
|
4731
4794
|
title: 'Assemble a report by naming blocks — no template, no markup',
|
|
4732
4795
|
description:
|
|
4733
4796
|
'A SELF-CONTAINED HTML REPORT, built by ENUMERATION. You pass an ordered list of blocks with plain text fields; every scrap of markup, styling, escaping and picture-embedding happens on our side. No template is accepted and none can be — a verb here names an element or a value from a closed list, never a program. ' +
|
|
4797
|
+
'IT IS NOT ONLY A BUG REPORT, and the block set is what makes that true. `finding` says something is wrong; `point` makes the same card with a tone of good · info · warn · bad and NO «expected» line, which is how a check that passed, a design note or a description of how a screen behaves gets written down; `section` gives a long document headings. A sign-off, a walkthrough for a developer, a page of observations for a designer and a defect list are the same paper here. ' +
|
|
4734
4798
|
'Pictures come from the shelf BY NAME and are flattened on the way in: the marks and the crop have been data up to that moment precisely so they could still be changed. ' +
|
|
4735
4799
|
'Print the result to get a PDF — the stylesheet is written for paper as well as for screen. ' +
|
|
4800
|
+
'PASS `save` TO KEEP IT IN THE EXTENSION: the panel’s Shots tab then lists it under Reports with Open and Save, and qa_reports / qa_report_get read it back by name later. Without it the document exists only in this reply and in whatever file you write. ' +
|
|
4736
4801
|
'IT REFUSES, and the refusals are the point. No blocks, a block naming a shot that is not on the shelf, or NOT ONE PICTURE anywhere in the document — each fails the whole build rather than quietly dropping a block: a page of assertions about an application with nothing showing any of it is the artefact this product exists to replace. ' +
|
|
4737
4802
|
'EXPECTED IS NEVER INVENTED. Leave `expected` out and the field says «not stated — a person fills this in». Filling it from your own guess turns a report into a hypothesis wearing the clothes of a protocol. ' +
|
|
4738
|
-
'The reply is an INVENTORY — blocks, pictures embedded, findings, bytes, warnings — not the word «done».',
|
|
4803
|
+
'The reply is an INVENTORY — blocks, pictures embedded, findings, points, bytes, warnings — not the word «done».',
|
|
4739
4804
|
inputSchema: {
|
|
4740
4805
|
tabId: tabIdArg,
|
|
4741
4806
|
out: z.string().optional().describe('Write the HTML here. Without it the document comes back inline, which for a report with pictures in it is usually the wrong trade.'),
|
|
4807
|
+
save: z
|
|
4808
|
+
.union([z.boolean(), z.string()])
|
|
4809
|
+
.optional()
|
|
4810
|
+
.describe('Keep it in the extension so a person can find it later in the panel. `true` names it after the page; a STRING names it yourself, and building again under the same name REPLACES that document rather than leaving a stale twin above it.'),
|
|
4811
|
+
kind: z
|
|
4812
|
+
.enum(['issues', 'verified', 'notes'])
|
|
4813
|
+
.optional()
|
|
4814
|
+
.describe('What this document IS, for the list in the panel: `issues` (things that are wrong — the default), `verified` (checked and it holds), `notes` (a description, no verdict). It changes no layout — the blocks do that.'),
|
|
4742
4815
|
blocks: z
|
|
4743
4816
|
.array(
|
|
4744
4817
|
z.union([
|
|
4745
4818
|
z.object({ block: z.literal('title'), text: z.string(), subtitle: z.string().optional() }),
|
|
4746
|
-
z.object({ block: z.literal('summary'), text: z.string().describe('One paragraph of plain words: what is
|
|
4819
|
+
z.object({ block: z.literal('summary'), text: z.string().describe('One paragraph of plain words: what this document is about.') }),
|
|
4820
|
+
z.object({ block: z.literal('section'), text: z.string().describe('A heading, so a document longer than one thought has a shape'), note: z.string().optional() }),
|
|
4747
4821
|
z.object({ block: z.literal('shot'), shot: z.string().describe('A name from qa_shot_list'), caption: z.string().optional() }),
|
|
4748
4822
|
z.object({
|
|
4749
4823
|
block: z.literal('finding'),
|
|
@@ -4754,6 +4828,14 @@ server.registerTool(
|
|
|
4754
4828
|
expected: z.string().optional().describe('Only if a PERSON said so. No verb here knows the requirements.'),
|
|
4755
4829
|
where: z.string().optional().describe('Selector or place'),
|
|
4756
4830
|
}),
|
|
4831
|
+
z.object({
|
|
4832
|
+
block: z.literal('point'),
|
|
4833
|
+
tone: z.enum(['good', 'info', 'warn', 'bad']).optional().describe('good = it holds · info = a neutral observation · warn · bad. Default info.'),
|
|
4834
|
+
title: z.string().describe('The line somebody will quote'),
|
|
4835
|
+
shot: z.string().optional().describe('The picture that shows it'),
|
|
4836
|
+
what: z.string().optional().describe('What is the case'),
|
|
4837
|
+
where: z.string().optional().describe('Selector or place'),
|
|
4838
|
+
}),
|
|
4757
4839
|
z.object({ block: z.literal('steps'), items: z.array(z.string()).describe('How to reproduce, short and with real values') }),
|
|
4758
4840
|
z.object({ block: z.literal('facts'), pairs: z.array(z.object({ name: z.string(), value: z.string() })) }),
|
|
4759
4841
|
z.object({ block: z.literal('note'), text: z.string(), tone: z.enum(['info', 'warn']).optional() }),
|
|
@@ -4762,26 +4844,107 @@ server.registerTool(
|
|
|
4762
4844
|
.describe('In document order. The conditions of the run — active mocks, suppressed dialogs — are added automatically and cannot be omitted by the caller.'),
|
|
4763
4845
|
},
|
|
4764
4846
|
},
|
|
4765
|
-
async ({ tabId, blocks, out }, extra) => {
|
|
4847
|
+
async ({ tabId, blocks, out, save, kind }, extra) => {
|
|
4766
4848
|
const bar = loader(extra, 'assembling the report');
|
|
4767
4849
|
try {
|
|
4768
|
-
const r = await callExtension('qa_report_build', { tabId, blocks }, 120_000);
|
|
4850
|
+
const r = await callExtension('qa_report_build', { tabId, blocks, save, kind }, 120_000);
|
|
4769
4851
|
if (!r || r.ok === false || typeof r.html !== 'string') return asText(r);
|
|
4852
|
+
const kept = r.saved
|
|
4853
|
+
? { saved: r.saved, savedNote: 'kept in the extension: the panel’s Shots tab lists it under Reports, and qa_reports reads it back by that name' }
|
|
4854
|
+
: {};
|
|
4770
4855
|
if (out) {
|
|
4771
4856
|
const full = resolve(out);
|
|
4772
4857
|
mkdirSync(dirname(full), { recursive: true });
|
|
4773
4858
|
writeFileSync(full, r.html, 'utf8');
|
|
4774
4859
|
/* The funnel's question has been answered by the act itself; it must not ask again. */
|
|
4775
4860
|
for (const [host, v] of reportFunnel) reportFunnel.set(host, { ...v, built: true });
|
|
4776
|
-
return asText({ ok: true, path: full, blocks: r.blocks, shotsEmbedded: r.shotsEmbedded, findings: r.findings, bytes: r.bytes, warnings: r.warnings, note: 'Open it and print to PDF; the structure is the same on paper.' });
|
|
4861
|
+
return asText({ ok: true, path: full, blocks: r.blocks, shotsEmbedded: r.shotsEmbedded, findings: r.findings, points: r.points, bytes: r.bytes, warnings: r.warnings, ...kept, note: 'Open it and print to PDF; the structure is the same on paper.' });
|
|
4777
4862
|
}
|
|
4778
|
-
return asText({ ok: true, blocks: r.blocks, shotsEmbedded: r.shotsEmbedded, findings: r.findings, bytes: r.bytes, warnings: r.warnings, html: r.html });
|
|
4863
|
+
return asText({ ok: true, blocks: r.blocks, shotsEmbedded: r.shotsEmbedded, findings: r.findings, points: r.points, bytes: r.bytes, warnings: r.warnings, ...kept, html: r.html });
|
|
4779
4864
|
} finally {
|
|
4780
4865
|
bar.stop();
|
|
4781
4866
|
}
|
|
4782
4867
|
},
|
|
4783
4868
|
);
|
|
4784
4869
|
|
|
4870
|
+
/**
|
|
4871
|
+
* THE REPORTS THE EXTENSION IS HOLDING.
|
|
4872
|
+
*
|
|
4873
|
+
* Three verbs and no fourth: list them, take one back, drop one. Writing is `qa_report_build`'s
|
|
4874
|
+
* `save` — a store with two ways in is a store whose two ways drift.
|
|
4875
|
+
*
|
|
4876
|
+
* The point of them is the hour AFTER the report was written. Until now a document left the
|
|
4877
|
+
* extension the moment it was built and Qiksy knew nothing about it afterwards, so «send me the
|
|
4878
|
+
* one from this morning» had no answer but «look in your Downloads». Now the panel lists it and
|
|
4879
|
+
* so does this, under the same name.
|
|
4880
|
+
*/
|
|
4881
|
+
server.registerTool(
|
|
4882
|
+
'qa_reports',
|
|
4883
|
+
{
|
|
4884
|
+
title: 'The reports the extension is keeping',
|
|
4885
|
+
description:
|
|
4886
|
+
'EVERY SAVED REPORT, BY NAME, with its title, what kind it is, when it was built, how many blocks and pictures went in and how big it is. ' +
|
|
4887
|
+
'These are the documents `qa_report_build { save }` kept, and they are the same ones the person sees in the panel — so «open the checkout one» means the same thing on both sides. ' +
|
|
4888
|
+
'It deliberately does NOT return the HTML: a report with pictures inside is megabytes, and the question here is which ones exist. Use qa_report_get for one. Read-only.',
|
|
4889
|
+
inputSchema: { tabId: tabIdArg },
|
|
4890
|
+
},
|
|
4891
|
+
async ({ tabId }) => {
|
|
4892
|
+
try {
|
|
4893
|
+
return asText(await callExtension('qa_reports', { tabId }, 20_000));
|
|
4894
|
+
} catch (e) {
|
|
4895
|
+
return asError(e);
|
|
4896
|
+
}
|
|
4897
|
+
},
|
|
4898
|
+
);
|
|
4899
|
+
|
|
4900
|
+
server.registerTool(
|
|
4901
|
+
'qa_report_get',
|
|
4902
|
+
{
|
|
4903
|
+
title: 'Take one saved report back out',
|
|
4904
|
+
description:
|
|
4905
|
+
'THE DOCUMENT ITSELF, by the name qa_reports lists. Pass `out` to write it to a file — which is almost always what you want, because a self-contained report carries its pictures as data and spending that through this conversation buys nothing. ' +
|
|
4906
|
+
'Without `out` the HTML comes back inline. Read-only: it takes a copy and leaves the extension’s own untouched.',
|
|
4907
|
+
inputSchema: {
|
|
4908
|
+
tabId: tabIdArg,
|
|
4909
|
+
name: z.string().describe('A name from qa_reports'),
|
|
4910
|
+
out: z.string().optional().describe('Write the HTML here instead of returning it inline'),
|
|
4911
|
+
},
|
|
4912
|
+
},
|
|
4913
|
+
async ({ tabId, name, out }) => {
|
|
4914
|
+
try {
|
|
4915
|
+
const r = await callExtension('qa_report_get', { tabId, name }, 30_000);
|
|
4916
|
+
if (!r || r.ok === false || typeof r.html !== 'string') return asText(r);
|
|
4917
|
+
if (out) {
|
|
4918
|
+
const full = resolve(out);
|
|
4919
|
+
mkdirSync(dirname(full), { recursive: true });
|
|
4920
|
+
writeFileSync(full, r.html, 'utf8');
|
|
4921
|
+
return asText({ ok: true, path: full, name: r.name, title: r.title, kind: r.kind, bytes: r.bytes, note: 'Open it and print to PDF; the structure is the same on paper.' });
|
|
4922
|
+
}
|
|
4923
|
+
return asText(r);
|
|
4924
|
+
} catch (e) {
|
|
4925
|
+
return asError(e);
|
|
4926
|
+
}
|
|
4927
|
+
},
|
|
4928
|
+
);
|
|
4929
|
+
|
|
4930
|
+
server.registerTool(
|
|
4931
|
+
'qa_report_drop',
|
|
4932
|
+
{
|
|
4933
|
+
title: 'Forget one saved report',
|
|
4934
|
+
description:
|
|
4935
|
+
'REMOVES A SAVED REPORT FROM THE EXTENSION — the row and the document together, because an index that forgets an entry while the megabytes stay is a leak nobody finds. ' +
|
|
4936
|
+
'It touches nothing on the page and nothing on disk: a file you already wrote with `out` is yours and stays. The reply lists what is left.',
|
|
4937
|
+
inputSchema: { tabId: tabIdArg, name: z.string().describe('A name from qa_reports') },
|
|
4938
|
+
},
|
|
4939
|
+
async ({ tabId, name }) => {
|
|
4940
|
+
try {
|
|
4941
|
+
return asText(await callExtension('qa_report_drop', { tabId, name }, 20_000));
|
|
4942
|
+
} catch (e) {
|
|
4943
|
+
return asError(e);
|
|
4944
|
+
}
|
|
4945
|
+
},
|
|
4946
|
+
);
|
|
4947
|
+
|
|
4785
4948
|
server.registerTool(
|
|
4786
4949
|
'qa_wiring',
|
|
4787
4950
|
{
|
|
@@ -4894,9 +5057,10 @@ server.registerTool(
|
|
|
4894
5057
|
'A MARK THAT KNOWS WHAT IT POINTS AT. Name the element — a CSS selector — and the box is looked up from the page itself at capture time, so the same instruction draws the correct picture today, after a fix, and at another screen width. ' +
|
|
4895
5058
|
'An arrow drawn on an ordinary screenshot points at a coordinate: right when it was drawn, wrong the moment anything re-renders. That is why annotated screenshots are disposable, and it is the whole difference here. ' +
|
|
4896
5059
|
'It is also how a picture marks ITSELF: a finding already carries a selector, so «draw the contrast failures» needs no aiming — pass their selectors straight through. ' +
|
|
4897
|
-
'Shapes: `box` (outline it), `pin` (a numbered dot at its corner — for ordered things like a tab route), `arrow` and `line` (from one element to another, which is how you show an error message wired to the wrong field). Colours are named, not free: default · bad · good · info · warn. ' +
|
|
5060
|
+
'Shapes: `box` (outline it), `pin` (a numbered dot at its corner — for ordered things like a tab route), `arrow` and `line` (from one element to another, which is how you show an error message wired to the wrong field), and `blur` (a pane that makes the element unreadable — an address, a customer\'s name, a token in a staging header). Colours are named, not free: default · bad · good · info · warn. ' +
|
|
5061
|
+
'A LABEL IS A CALLOUT, not a sticker: it is placed in free space beside its element, clear of every other mark, and joined to the outline by a leader line ending in a dot. So «outline the payment block and say what is wrong with it» comes back as one readable statement instead of words stamped over the thing they are about. ' +
|
|
4898
5062
|
'A mark whose element is not on the page is REPORTED in `missed`, never silently dropped — a reader who sees three arrows must be able to trust that there were three things to point at. ' +
|
|
4899
|
-
'Read-only: it photographs and draws, and touches nothing.',
|
|
5063
|
+
'Read-only: it photographs and draws, and touches nothing — a blur hides the data in THIS picture, never on the page, so the next capture shows it again.',
|
|
4900
5064
|
inputSchema: {
|
|
4901
5065
|
tabId: tabIdArg,
|
|
4902
5066
|
fromFindings: z
|
|
@@ -4906,13 +5070,15 @@ server.registerTool(
|
|
|
4906
5070
|
marks: z
|
|
4907
5071
|
.array(
|
|
4908
5072
|
z.object({
|
|
4909
|
-
kind: z.enum(['box', 'pin', 'arrow', 'line']).optional().describe("Default 'box'"),
|
|
5073
|
+
kind: z.enum(['box', 'pin', 'arrow', 'line', 'blur']).optional().describe("Default 'box'"),
|
|
4910
5074
|
selector: z.string().optional().describe('The element to mark'),
|
|
4911
5075
|
box: z.object({ x: z.number(), y: z.number(), w: z.number(), h: z.number() }).optional().describe('Or a box in IMAGE pixels, if you already have one from a shot map'),
|
|
4912
5076
|
toSelector: z.string().optional().describe('For arrow/line: the element at the other end'),
|
|
4913
5077
|
toBox: z.object({ x: z.number(), y: z.number(), w: z.number(), h: z.number() }).optional(),
|
|
4914
|
-
label: z.string().optional().describe('Short text on the mark
|
|
5078
|
+
label: z.string().optional().describe('Short text on the mark — three characters inside a pin, up to sixty in a callout placed beside the element and joined to it by a leader line'),
|
|
4915
5079
|
colour: z.enum(['default', 'bad', 'good', 'info', 'warn']).optional(),
|
|
5080
|
+
strength: z.number().optional().describe("For 'blur': how hard it hides, 4–60 (default 14)"),
|
|
5081
|
+
pad: z.number().optional().describe("For 'blur': extra margin round the element, in CSS pixels (default 2) — a pane flush with the text tends to leave the tops of the letters showing"),
|
|
4916
5082
|
}),
|
|
4917
5083
|
)
|
|
4918
5084
|
.optional()
|
|
@@ -4963,12 +5129,12 @@ server.registerTool(
|
|
|
4963
5129
|
const { resolve } = await import('node:path');
|
|
4964
5130
|
const full = resolve(outPath);
|
|
4965
5131
|
writeFileSync(full, Buffer.from(b64, 'base64'));
|
|
4966
|
-
return asText({ ok: true, path: full, drawn: r.drawn, missed: r.missed, ...(skipped ? { findingsWithoutAPlace: skipped, findingsNote: `${skipped} finding(s) have no selector — a console error or a failed request is not AT a place on the page. They are in qa_findings, not in this picture.` } : {}), width: r.width, height: r.height, note: r.note });
|
|
5132
|
+
return asText({ ok: true, path: full, drawn: r.drawn, ...(r.hidden ? { hidden: r.hidden } : {}), missed: r.missed, ...(skipped ? { findingsWithoutAPlace: skipped, findingsNote: `${skipped} finding(s) have no selector — a console error or a failed request is not AT a place on the page. They are in qa_findings, not in this picture.` } : {}), width: r.width, height: r.height, note: r.note });
|
|
4967
5133
|
}
|
|
4968
5134
|
return {
|
|
4969
5135
|
content: [
|
|
4970
5136
|
{ type: 'image', data: b64, mimeType: 'image/png' },
|
|
4971
|
-
{ type: 'text', text: JSON.stringify({ ok: true, drawn: r.drawn, missed: r.missed, ...(skipped ? { findingsWithoutAPlace: skipped } : {}), width: r.width, height: r.height, note: r.note }, null, 2) },
|
|
5137
|
+
{ type: 'text', text: JSON.stringify({ ok: true, drawn: r.drawn, ...(r.hidden ? { hidden: r.hidden } : {}), missed: r.missed, ...(skipped ? { findingsWithoutAPlace: skipped } : {}), width: r.width, height: r.height, note: r.note }, null, 2) },
|
|
4972
5138
|
],
|
|
4973
5139
|
};
|
|
4974
5140
|
} catch (e) {
|
|
@@ -6482,6 +6648,53 @@ server.registerTool(
|
|
|
6482
6648
|
},
|
|
6483
6649
|
);
|
|
6484
6650
|
|
|
6651
|
+
/**
|
|
6652
|
+
* THE FULL STOP — the one thing a browser cannot work out for itself.
|
|
6653
|
+
*
|
|
6654
|
+
* Somebody sets an agent off on a long run and goes to do something else: another tab, another
|
|
6655
|
+
* screen, another room. Until now nothing told them it had ended. «No calls for a while» is not
|
|
6656
|
+
* the answer — a model that stops to think looks exactly like a model that has finished, and a
|
|
6657
|
+
* signal that cries wolf every thirty seconds gets switched off within the hour.
|
|
6658
|
+
*
|
|
6659
|
+
* So the run says so itself, and the browser turns that into something visible from across a
|
|
6660
|
+
* room: the mark on the tab pulses slowly — and stops the moment they look at that tab, because
|
|
6661
|
+
* being looked at IS the acknowledgement. A tab already in front is never pulsed at all.
|
|
6662
|
+
*/
|
|
6663
|
+
server.registerTool(
|
|
6664
|
+
'qa_done',
|
|
6665
|
+
{
|
|
6666
|
+
title: 'Say the run has finished',
|
|
6667
|
+
description:
|
|
6668
|
+
'CALL THIS WHEN THE WORK IS ACTUALLY OVER — the last check made, the last fix verified, the answer about to be written. The mark on every driven tab the person is NOT looking at then pulses slowly, so somebody who walked away from the screen can see from across the room that it ended. It stops by itself when they look at that tab; there is nothing to dismiss. ' +
|
|
6669
|
+
'It is a nudge, not a report: still say what you did, in your own words. And it is not a progress beat — calling it between steps teaches people to ignore it, which costs the signal its only job. Any further call cancels it, so a run that continues quietly withdraws it. ' +
|
|
6670
|
+
'A short sound plays as well, for somebody who is not looking at the browser at all — the person can switch the sound off in the popup, and the pulse stays either way. ' +
|
|
6671
|
+
'Free, and it touches nothing on the page.',
|
|
6672
|
+
inputSchema: {
|
|
6673
|
+
tabId: tabIdArg,
|
|
6674
|
+
verdict: z
|
|
6675
|
+
.enum(['ok', 'problem'])
|
|
6676
|
+
.optional()
|
|
6677
|
+
.describe('How it ended. `ok` (default) rises; `problem` falls — the same sound in two directions, so the ear tells them apart without being taught. Say what you actually found; a run that discovered a defect ended FINE if you finished the work.'),
|
|
6678
|
+
},
|
|
6679
|
+
},
|
|
6680
|
+
async ({ tabId, verdict }) => {
|
|
6681
|
+
try {
|
|
6682
|
+
return asText(await callExtension('qa_done', { tabId, verdict }, 15_000));
|
|
6683
|
+
} catch (e) {
|
|
6684
|
+
/* The verb lives in the EXTENSION, which updates on the Web Store's clock while this server
|
|
6685
|
+
updates with npx — «unknown tool» is the shape of that gap, and raw it reads as a bug. */
|
|
6686
|
+
if (/Unknown tool/i.test(String(e?.message || e))) {
|
|
6687
|
+
return asText({
|
|
6688
|
+
ok: false,
|
|
6689
|
+
error: 'not-in-this-extension-build',
|
|
6690
|
+
note: 'qa_done needs a newer extension in the browser. Nothing is lost: say you have finished in your own words, which is what the person reads anyway.',
|
|
6691
|
+
});
|
|
6692
|
+
}
|
|
6693
|
+
return asError(e);
|
|
6694
|
+
}
|
|
6695
|
+
},
|
|
6696
|
+
);
|
|
6697
|
+
|
|
6485
6698
|
server.registerTool(
|
|
6486
6699
|
'qa_close_tab',
|
|
6487
6700
|
{
|