@usercase/mcp 0.2.11-next.3 → 0.2.11-next.4
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 +45 -0
- package/agents/fix-issue.md +40 -0
- package/agents/fix.md +5 -1
- package/mcp/hub.js +8 -2
- package/mcp/path.js +4 -2
- package/mcp/report.js +7 -0
- package/mcp/server.js +111 -7
- package/package.json +1 -1
- package/src/browser/study.js +62 -5
package/README.md
CHANGED
|
@@ -30,6 +30,21 @@ Each repository keeps its own connection, so different sites can go to different
|
|
|
30
30
|
|
|
31
31
|
Prefer a key? Create one under **Settings → Integrations → API keys** and add `-e USERCASE_API_KEY=eb_sk_...` before `--`. A key you set this way is used first.
|
|
32
32
|
|
|
33
|
+
## Commands
|
|
34
|
+
|
|
35
|
+
| Command | What it does |
|
|
36
|
+
|---|---|
|
|
37
|
+
| `/mcp__usercase__research <url> [count]` | Sends virtual users to your site. [More](#run-a-research) |
|
|
38
|
+
| `/mcp__usercase__install` | Adds the Usercase script to your site. [More](#install-the-script) |
|
|
39
|
+
| `/mcp__usercase__new-research` | Plans a research from your data and code and saves it as a draft. [More](#planning-a-research) |
|
|
40
|
+
| `/mcp__usercase__path <research id>` | Records the route each task is meant to take. [More](#record-a-tasks-expected-path) |
|
|
41
|
+
| `/mcp__usercase__fix <research id> <finding>` | Fixes a finding from a research report, or an issue from Issues, in your code. [More](#fix-a-finding-from-a-report) |
|
|
42
|
+
| `/mcp__usercase__question` | Suggests a question for a precise moment and saves it as a draft. [More](#questions-for-a-precise-moment) |
|
|
43
|
+
| `/mcp__usercase__steps <window id>` | Writes the steps for a question you are making in the dashboard. [More](#questions-for-a-precise-moment) |
|
|
44
|
+
| `/mcp__usercase__connect` | Connects this repository to another project. [More](#connect) |
|
|
45
|
+
|
|
46
|
+
Wherever a command needs an id, the dashboard shows it ready to copy. You can also just ask Claude, for example "research my staging site with 10 virtual users".
|
|
47
|
+
|
|
33
48
|
## Install the script
|
|
34
49
|
|
|
35
50
|
The survey cards come from the Usercase script on your site. If it isn't there yet, let Claude Code add it:
|
|
@@ -67,6 +82,14 @@ The number is how many people visit in total (default 5). Above 5, Claude works
|
|
|
67
82
|
|
|
68
83
|
With `unlimited` it keeps going until you press Esc. Every visit uses your Claude usage.
|
|
69
84
|
|
|
85
|
+
### Focus on one part of the site
|
|
86
|
+
|
|
87
|
+
Name a part of the site, and Claude casts only people who come for it:
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
/mcp__usercase__research https://staging.example.com 5 focus=checkout
|
|
91
|
+
```
|
|
92
|
+
|
|
70
93
|
### Take part in a research
|
|
71
94
|
|
|
72
95
|
Give a research id from your dashboard and the people take part in it instead of exploring freely — they accept its invitation and do its tasks from the study panel, the way invited participants do. The research must be live.
|
|
@@ -75,6 +98,8 @@ Give a research id from your dashboard and the people take part in it instead of
|
|
|
75
98
|
/mcp__usercase__research https://staging.example.com 10 research=<research id>
|
|
76
99
|
```
|
|
77
100
|
|
|
101
|
+
If the research is moderated by voice, the people read the moderator's captions and answer out loud, with a voice already on your computer (Windows voices, macOS `say` or `espeak`). No outside voice service is used.
|
|
102
|
+
|
|
78
103
|
When a visit ends, it is sent to your dashboard (**Visits**). If the upload fails, it is kept in `.usercase/outbox.jsonl` and sent with the next one.
|
|
79
104
|
|
|
80
105
|
### Sites behind a sign-in
|
|
@@ -119,6 +144,26 @@ Claude suggests the moment, the question and its answers, and saves it only afte
|
|
|
119
144
|
|
|
120
145
|
Claude asks whether you already know what you want to learn. If you don't, it reads your project's findings and where visitors left, and suggests two or three researches, saying which evidence comes from real visitors and which from virtual users. With no data yet, it offers to send a few virtual users first. It then reads your code to write the tasks, their success conditions (the page that means done, or the exact button) and the route each task is meant to take, and shows you all of it at once. On your approval it walks each route in a browser, checks it, and saves the research with the routes — as a draft in your Inbox, where you create it. The routes move into the research when you create it. A draft you discard, or don't create within 30 days, loses its recorded paths and their screenshots.
|
|
121
146
|
|
|
147
|
+
## Fix a finding from a report
|
|
148
|
+
|
|
149
|
+
A research's **Report** lists findings: problems people hit, each backed by what they did, what the screen showed and what they wrote. Each finding has a **Fix with your agent** command:
|
|
150
|
+
|
|
151
|
+
```
|
|
152
|
+
/mcp__usercase__fix <research id> <finding, e.g. F1>
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Add `real` for the report of real people; without it, the report of virtual users is used. Claude reads the finding and any earlier fixes of it, finds the code behind it, and tells you the change it proposes before touching anything. After you approve, it makes the change, runs your checks and records the fix on the finding. It doesn't commit, push or open a pull request unless you ask.
|
|
156
|
+
|
|
157
|
+
Once the change is deployed, mark it **Live on site** on the finding: the report compares people before and after the fix only from then on. To check it with virtual users, run the same tasks again with `research=<research id>`.
|
|
158
|
+
|
|
159
|
+
Issues from your questions' answers have the same button in **Issues**:
|
|
160
|
+
|
|
161
|
+
```
|
|
162
|
+
/mcp__usercase__fix issue <issue id>
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Claude reads the question, what people chose and wrote, and any other issues with the same cause, then works the same way. Once the change is deployed, mark the issue reviewed; the question keeps running, so later answers show whether people still hit it.
|
|
166
|
+
|
|
122
167
|
## What the visitors will and won't do
|
|
123
168
|
|
|
124
169
|
- They identify themselves on every request (see below), and their answers are counted separately from real visitors. This cannot be turned off.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Fix an issue found from your visitors' answers
|
|
2
|
+
|
|
3
|
+
Issue: {{issue}}
|
|
4
|
+
|
|
5
|
+
Usercase asks short questions on this site and groups the answers into issues — problems on the site, each backed by the question, what people chose and what they wrote. You fix one issue in this repository's code.
|
|
6
|
+
|
|
7
|
+
## 1. Read the issue
|
|
8
|
+
|
|
9
|
+
Call `issue_read` with the issue id. If no issue id is given above, call `issue_list` first, show the user the open issues (title, where, whether it is a clear fix or the cause is unclear, and whether someone is already fixing it), and ask which one to fix.
|
|
10
|
+
|
|
11
|
+
Read all of it: the question and the page it was asked on, what people chose, what they wrote, the AI's reading of it and the suggested next step. The suggestion was written without seeing the code; treat it as a direction, not a spec.
|
|
12
|
+
|
|
13
|
+
If the cause is marked unclear, say so to the user: the answers show the problem but not why. Propose the change only if the code makes the cause clear; otherwise suggest running a research on it instead ({{research_cmd}}).
|
|
14
|
+
|
|
15
|
+
If it lists other issues with the same cause, tell the user — one change may fix them together.
|
|
16
|
+
|
|
17
|
+
If it lists earlier fixes, read what each changed. Do not repeat an approach that is already in place; say how your change differs. If an earlier fix is probably not deployed yet, tell the user — they may only need to deploy it.
|
|
18
|
+
|
|
19
|
+
## 2. Find the code
|
|
20
|
+
|
|
21
|
+
From the page, the question and what people wrote, find the code behind it: routes, the page components, the elements people talk about, the text shown. Confirm you found the right place by matching what people mention (a section, a button, a label) to the code.
|
|
22
|
+
|
|
23
|
+
## 3. Propose the change — before changing anything
|
|
24
|
+
|
|
25
|
+
Tell the user, briefly:
|
|
26
|
+
- what in the code causes the problem, with file paths,
|
|
27
|
+
- the change you propose and why it addresses what people chose and wrote, and
|
|
28
|
+
- anything the issue suggests that you would not do, and why (it would break something, it needs a product decision, the code already does it).
|
|
29
|
+
|
|
30
|
+
Keep the change as small as solves the problem. Do not redesign unrelated parts. Wait for the user's approval or corrections.
|
|
31
|
+
|
|
32
|
+
## 4. Change the code
|
|
33
|
+
|
|
34
|
+
When the user approves, call `issue_fix` with `status` `fixing` and the files you will change. Then make the change. Follow the repository's own conventions, and run its checks (type check, lint, tests) if it has them.
|
|
35
|
+
|
|
36
|
+
## 5. Record the fix
|
|
37
|
+
|
|
38
|
+
When the change is done, call `issue_fix` with `status` `fixed`, the files changed, a one-sentence summary written for the team (what a visitor now sees differently), and the pull request link if you opened one. Do not commit, push or open a pull request unless the user asks.
|
|
39
|
+
|
|
40
|
+
Then tell the user that the issue shows as fixed in Issues, and that once the change is deployed they mark it reviewed there. The question keeps running, so later answers show whether people still hit it.
|
package/agents/fix.md
CHANGED
|
@@ -12,6 +12,8 @@ Call `report_finding` with the research id, the finding key and the report. If n
|
|
|
12
12
|
|
|
13
13
|
Read all of it: the title and why, who it hit, the screen checks (what the page really showed — these come from the recordings, not from opinions), the paths people took, the groups of what they wrote, and the suggested changes. The suggested changes were written without seeing the code; treat them as a direction, not a spec.
|
|
14
14
|
|
|
15
|
+
If it lists earlier fixes of this finding, read what each changed and how it went. Do not repeat an approach that made things worse or changed nothing; say how your change differs. If an earlier fix is not live yet or probably not deployed, tell the user — they may only need to deploy it.
|
|
16
|
+
|
|
15
17
|
## 2. Find the code
|
|
16
18
|
|
|
17
19
|
From the pages, buttons and texts in the evidence, find the code behind them: routes, the page components, the button and its click handler, the text shown. Confirm you found the right place by matching the exact text on screen (a button label, a heading) to the code.
|
|
@@ -31,6 +33,8 @@ When the user approves, call `report_fix` with `status` `fixing` and the files y
|
|
|
31
33
|
|
|
32
34
|
## 5. Record the fix
|
|
33
35
|
|
|
34
|
-
When the change is done, call `report_fix` with `status` `fixed`, the files changed, a one-sentence summary written for the team (what a visitor now sees differently), and the pull request link if you opened one. Do not commit, push or open a pull request unless the user asks.
|
|
36
|
+
When the change is done, call `report_fix` with `status` `fixed`, the files changed, a one-sentence summary written for the team (what a visitor now sees differently — the next report and the next fix read it), and the pull request link if you opened one. Do not commit, push or open a pull request unless the user asks.
|
|
37
|
+
|
|
38
|
+
The report compares people only after the fix is live on the site. Tell the user to mark it **Live on site** on the finding once it is deployed; when a new version of their site shows up in sessions, the report also asks whether this fix is in it.
|
|
35
39
|
|
|
36
40
|
Then tell the user that the finding shows as fixed in the report, and how to check it worked: {{check}}
|
package/mcp/hub.js
CHANGED
|
@@ -721,9 +721,15 @@ class Hub {
|
|
|
721
721
|
}
|
|
722
722
|
if (p.kind === "timeout") {
|
|
723
723
|
const skip = decision.action === "choose" && decision.choiceIndex === 1;
|
|
724
|
-
|
|
724
|
+
// 눌리지 않았으면 그대로 알린다 — 전에는 실패를 넘기고 "골랐다"고 해서 같은 창에 갇혔다(11/05)
|
|
725
|
+
let moved = null;
|
|
726
|
+
try {
|
|
727
|
+
moved = await study.press(page, skip ? "timeoutSkip" : "keepGoing");
|
|
728
|
+
} catch (e) {
|
|
729
|
+
return [`You pressed "${skip ? "Skip" : "Continue"}", but it did not go through: ${String(e.message).split("\n")[0]}. The study is still asking.`, "", await this.screen(rec)].join("\n");
|
|
730
|
+
}
|
|
725
731
|
if (skip) st.skipped.push(p.task);
|
|
726
|
-
said = skip ? `You chose to skip the task "${p.task}".` : "You chose to keep trying.";
|
|
732
|
+
said = (skip ? `You chose to skip the task "${p.task}".` : "You chose to keep trying.") + (moved ? ` (${moved} was covering the study panel, so you dragged the panel aside first.)` : "");
|
|
727
733
|
} else {
|
|
728
734
|
const q = p.q;
|
|
729
735
|
const ans = decision.action === "choose" ? { index: decision.choiceIndex } : decision.action === "other" ? { text: decision.otherText } : {};
|
package/mcp/path.js
CHANGED
|
@@ -91,7 +91,9 @@ function describeInPage(n, { sel: pickSel, lift = false }) {
|
|
|
91
91
|
} catch (e) {}
|
|
92
92
|
// SDK 는 id 를 이스케이프 없이 `#id` 로 쓴다 — 평범한 id 만(React useId 의 ":r1:" 같은 건 SDK 에서 오류가 난다)
|
|
93
93
|
const ids = pick.id && /^[A-Za-z][\w-]*$/.test(pick.id) && document.querySelectorAll("#" + pick.id).length === 1 ? [pick.id] : [];
|
|
94
|
-
|
|
94
|
+
// 입력칸에 친 값은 이름으로 안 쓴다(방문자 정보·비밀번호가 들어간다). submit·button 의 value 는 버튼 글자라 쓴다.
|
|
95
|
+
// 아래 path_check 의 label 과 같은 식이어야 한다 — 다르면 같은 요소를 "이름이 다르다"로 본다
|
|
96
|
+
const label = (pick.getAttribute("aria-label") || pick.innerText || (pick.tagName === "INPUT" && /^(submit|button|reset)$/i.test(pick.type) ? pick.value : "") || pick.getAttribute("title") || pick.getAttribute("placeholder") || "").replace(/\s+/g, " ").trim().slice(0, 80);
|
|
95
97
|
return { xpath: xp(pick), classes, ids, label, tag: pick.tagName.toLowerCase() };
|
|
96
98
|
}
|
|
97
99
|
|
|
@@ -366,7 +368,7 @@ class PathRecorder {
|
|
|
366
368
|
if (!ok) break;
|
|
367
369
|
const h = (await page.evaluateHandle(byXPathInPage, a.xpath)).asElement();
|
|
368
370
|
const visible = h ? await h.isVisible().catch(() => false) : false;
|
|
369
|
-
const label = h ? await h.evaluate((n) => (n.getAttribute("aria-label") || n.innerText || n.value || n.getAttribute("title") || "").replace(/\s+/g, " ").trim().slice(0, 80)) : "";
|
|
371
|
+
const label = h ? await h.evaluate((n) => (n.getAttribute("aria-label") || n.innerText || (n.tagName === "INPUT" && /^(submit|button|reset)$/i.test(n.type) ? n.value : "") || n.getAttribute("title") || n.getAttribute("placeholder") || "").replace(/\s+/g, " ").trim().slice(0, 80)) : "";
|
|
370
372
|
const no = a.step !== null ? `${a.step + 1}. ` : " ";
|
|
371
373
|
if (!h || !visible) {
|
|
372
374
|
lines.push(`✗ ${no}${a.do} "${a.label || a.text || ""}" — ${h ? "found but not visible" : "not found at its recorded position"} (${a.xpath})`);
|
package/mcp/report.js
CHANGED
|
@@ -297,6 +297,10 @@ const rq = (audience) => (audience === "real" ? "?audience=real" : "?audience=vi
|
|
|
297
297
|
const getReport = (researchId, audience) => keyed("GET", `/agent/researches/${encodeURIComponent(researchId)}/report${rq(audience)}`);
|
|
298
298
|
const getFinding = (researchId, key, audience) => keyed("GET", `/agent/researches/${encodeURIComponent(researchId)}/report/findings/${encodeURIComponent(key)}${rq(audience)}`);
|
|
299
299
|
const saveFix = (researchId, key, body) => keyed("PUT", `/agent/researches/${encodeURIComponent(researchId)}/report/findings/${encodeURIComponent(key)}/fix`, body);
|
|
300
|
+
/** Issues(fix issue) — 열린 문제 목록, 문제 하나와 근거, 고친 기록 */
|
|
301
|
+
const getIssues = () => keyed("GET", "/agent/issues");
|
|
302
|
+
const getIssue = (id) => keyed("GET", `/agent/issues/${encodeURIComponent(id)}`);
|
|
303
|
+
const saveIssueFix = (id, body) => keyed("PUT", `/agent/issues/${encodeURIComponent(id)}/fix`, body);
|
|
300
304
|
/** 대시보드 주소. API 주소에서 /koa 를 뗀다 */
|
|
301
305
|
const dashboardUrl = (p) => apiBase().replace(/\/koa$/, "") + p;
|
|
302
306
|
|
|
@@ -323,5 +327,8 @@ module.exports = {
|
|
|
323
327
|
getReport,
|
|
324
328
|
getFinding,
|
|
325
329
|
saveFix,
|
|
330
|
+
getIssues,
|
|
331
|
+
getIssue,
|
|
332
|
+
saveIssueFix,
|
|
326
333
|
dashboardUrl,
|
|
327
334
|
};
|
package/mcp/server.js
CHANGED
|
@@ -609,6 +609,15 @@ server.registerTool(
|
|
|
609
609
|
if (e.words.length) L.push("", "What people wrote (grouped by meaning, with examples):", ...e.words.map((w) => `- ${w.text}`));
|
|
610
610
|
if (e.numbers.length) L.push("", "Numbers:", ...e.numbers.map((x) => `- ${x}`));
|
|
611
611
|
L.push("", "Suggested changes (written without seeing the code):", ...f.changes.map((c, i) => `${i + 1}. ${c}`), `Check after the fix: ${f.measure}`);
|
|
612
|
+
// 지난 고침 — 무엇을 바꿨고 어땠나. 같은 방법을 되풀이하지 않게
|
|
613
|
+
const VERDICT = { better: "better", worse: "worse", mixed: "mixed", same: "no change", waiting: "not compared yet", not_deployed: "probably not deployed — sessions after it still ran the old version" };
|
|
614
|
+
if (r.history && r.history.length) {
|
|
615
|
+
L.push("", "Earlier fixes of this finding (each compared with the version right before it):");
|
|
616
|
+
for (const h of r.history) {
|
|
617
|
+
L.push(`- Fix ${h.attempt} by ${h.by}: ${h.summary || "(no note)"} — ${h.liveAt ? `live since ${String(h.liveAt).slice(0, 16).replace("T", " ")}` : "not live yet"}, ${VERDICT[h.verdict] || h.verdict}`);
|
|
618
|
+
for (const row of h.rows || []) L.push(` · ${row.label}: ${row.before[0]}/${row.before[1]} → ${row.after[0]}/${row.after[1]}`);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
612
621
|
if (r.sessionsUrl) L.push("", `Watch these sessions: ${r.sessionsUrl}`);
|
|
613
622
|
L.push(`Report: ${r.reportUrl}`);
|
|
614
623
|
return text(L.join("\n"));
|
|
@@ -619,7 +628,7 @@ server.registerTool(
|
|
|
619
628
|
"report_fix",
|
|
620
629
|
{
|
|
621
630
|
description:
|
|
622
|
-
"Record on the research report that you are fixing a finding (status fixing, once the user approved the change) or have fixed it (status fixed, with the files, a one-sentence summary for the team, and the pull request link if any). The report
|
|
631
|
+
"Record on the research report that you are fixing a finding (status fixing, once the user approved the change) or have fixed it (status fixed, with the files, a one-sentence summary for the team, and the pull request link if any). If the last fix of this finding is already live on the site, this starts the next fix (fix 2, 3 …); otherwise it adds to the current one. The report compares people after each fix goes live with the version right before it.",
|
|
623
632
|
inputSchema: {
|
|
624
633
|
research: z.string().describe("The research id"),
|
|
625
634
|
finding: z.string().describe("Finding key, e.g. F1"),
|
|
@@ -633,7 +642,86 @@ server.registerTool(
|
|
|
633
642
|
wrap(async ({ research, finding, audience, status, files, summary, prUrl }) => {
|
|
634
643
|
const agent = CODEX ? "Codex" : OTHER ? null : "Claude Code";
|
|
635
644
|
const r = await report.saveFix(research, finding, { audience: audience || "virtual", status, agent, files, summary, prUrl });
|
|
636
|
-
return text(`${r.key} is marked ${r.status} on the report: ${r.reportUrl}`);
|
|
645
|
+
return text(`${r.key} fix ${r.attempt} is marked ${r.status} on the report: ${r.reportUrl}`);
|
|
646
|
+
}),
|
|
647
|
+
);
|
|
648
|
+
|
|
649
|
+
// ── Issues 의 문제 고치기(fix issue) ──
|
|
650
|
+
const issueFixLine = (f) => (f ? ` — ${f.status === "fixed" ? "fixed" : "being fixed"}${f.agent ? ` by ${f.agent}` : ""}` : "");
|
|
651
|
+
|
|
652
|
+
server.registerTool(
|
|
653
|
+
"issue_list",
|
|
654
|
+
{
|
|
655
|
+
description: "List the open issues of the Usercase project (problems found from visitors' answers to questions on the site): id, title, where, whether it is a clear fix, and whether one is being fixed. Use when the fix command has no issue id.",
|
|
656
|
+
inputSchema: {},
|
|
657
|
+
},
|
|
658
|
+
wrap(async () => {
|
|
659
|
+
const r = await report.getIssues();
|
|
660
|
+
if (!r.issues.length) return text("No open issues in this project.");
|
|
661
|
+
return text(
|
|
662
|
+
[
|
|
663
|
+
...r.issues.map(
|
|
664
|
+
(i) =>
|
|
665
|
+
`${i.id} · ${i.title} — ${i.page || "any page"}, ${i.answers} ${i.from === "virtual" ? "virtual users" : "answers"}, ${i.nextStep === "fix" ? "clear fix" : "cause unclear"}${i.status === "acknowledged" ? ", reviewed" : ""}${issueFixLine(i.fix)}`,
|
|
666
|
+
),
|
|
667
|
+
"",
|
|
668
|
+
`Issues: ${r.url}`,
|
|
669
|
+
].join("\n"),
|
|
670
|
+
);
|
|
671
|
+
}),
|
|
672
|
+
);
|
|
673
|
+
|
|
674
|
+
server.registerTool(
|
|
675
|
+
"issue_read",
|
|
676
|
+
{
|
|
677
|
+
description:
|
|
678
|
+
"Read one issue of the Usercase project with all its evidence: the question and page, what people chose, what they wrote, the AI's reading, other issues with the same cause, and earlier fixes. Read it before looking for the code.",
|
|
679
|
+
inputSchema: { issue: z.string().describe("The issue id, as in the dashboard command") },
|
|
680
|
+
},
|
|
681
|
+
wrap(async ({ issue }) => {
|
|
682
|
+
const r = await report.getIssue(String(issue).trim());
|
|
683
|
+
const i = r.issue;
|
|
684
|
+
const e = r.evidence;
|
|
685
|
+
const L = [
|
|
686
|
+
`${i.title}${issueFixLine(i.fix)}`,
|
|
687
|
+
`From ${i.answers} ${i.from === "virtual" ? "virtual users' answers" : "visitors' answers"} · ${i.nextStep === "fix" ? "clear fix" : "cause unclear — the answers show the problem, not why"}`,
|
|
688
|
+
"",
|
|
689
|
+
`Question: "${r.question.text}" (asked on ${r.question.page || "any page"})`,
|
|
690
|
+
"",
|
|
691
|
+
`What the answers show: ${i.observation}`,
|
|
692
|
+
`Likely why: ${i.hypothesis}`,
|
|
693
|
+
];
|
|
694
|
+
if (i.nextStepNote) L.push(`Suggested next step (written without seeing the code): ${i.nextStepNote}`);
|
|
695
|
+
if (e.distribution.length) L.push("", "What people chose:", ...e.distribution.map((d) => `- ${d.label}: ${d.n}${e.focus.includes(d.label) ? " ← this issue" : ""}`));
|
|
696
|
+
if (e.words.length) L.push("", "What people wrote:", ...e.words.map((w) => `- "${w.text}"${w.n > 1 ? ` (${w.n} people)` : ""}${w.page ? ` (on ${w.page}${w.device ? `, ${w.device}` : ""})` : ""}`));
|
|
697
|
+
if (e.pages.length) L.push("", `Pages: ${e.pages.map((p) => `${p.page} (${p.n})`).join(", ")}`);
|
|
698
|
+
if (r.cause) L.push("", `Same cause as other issues — "${r.cause.title}": ${r.cause.summary}`, ...r.cause.others.map((o) => `- ${o.id} · ${o.title}${o.page ? ` (${o.page})` : ""}`));
|
|
699
|
+
if (r.history.length) {
|
|
700
|
+
L.push("", "Earlier fixes of this issue:");
|
|
701
|
+
for (const h of r.history) L.push(`- Fix ${h.attempt} by ${h.by} (${h.status}): ${h.summary || "(no note)"}${h.files.length ? ` — ${h.files.join(", ")}` : ""}`);
|
|
702
|
+
}
|
|
703
|
+
L.push("", `Issue: ${r.url}`);
|
|
704
|
+
return text(L.join("\n"));
|
|
705
|
+
}),
|
|
706
|
+
);
|
|
707
|
+
|
|
708
|
+
server.registerTool(
|
|
709
|
+
"issue_fix",
|
|
710
|
+
{
|
|
711
|
+
description:
|
|
712
|
+
"Record on a Usercase issue that you are fixing it (status fixing, once the user approved the change) or have fixed it (status fixed, with the files, a one-sentence summary for the team, and the pull request link if any). After a finished fix, starting again records the next fix.",
|
|
713
|
+
inputSchema: {
|
|
714
|
+
issue: z.string().describe("The issue id"),
|
|
715
|
+
status: z.enum(["fixing", "fixed"]),
|
|
716
|
+
files: z.array(z.string()).optional().describe("Repository paths of the files changed (or to be changed)"),
|
|
717
|
+
summary: z.string().optional().describe("One sentence for the team: what visitors now see differently"),
|
|
718
|
+
prUrl: z.string().optional().describe("Pull request link, if one was opened"),
|
|
719
|
+
},
|
|
720
|
+
},
|
|
721
|
+
wrap(async ({ issue, status, files, summary, prUrl }) => {
|
|
722
|
+
const agent = CODEX ? "Codex" : OTHER ? null : "Claude Code";
|
|
723
|
+
const r = await report.saveIssueFix(String(issue).trim(), { status, agent, files, summary, prUrl });
|
|
724
|
+
return text(`"${r.title}" fix ${r.attempt} is marked ${r.status}: ${r.url}`);
|
|
637
725
|
}),
|
|
638
726
|
);
|
|
639
727
|
|
|
@@ -1018,6 +1106,14 @@ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
|
1018
1106
|
*/
|
|
1019
1107
|
function byName(args = {}, schema = {}) {
|
|
1020
1108
|
const names = Object.keys(schema || {});
|
|
1109
|
+
// fix 의 `issue <id>` — 띄어쓰기로 research="issue", finding="<id>" 로 나뉘어 온다. 그대로 두면 아래에서 id 가 research 로 간다
|
|
1110
|
+
if (names.includes("issue")) {
|
|
1111
|
+
const vals = Object.values(args || {}).map((v) => (v == null ? "" : String(v).trim()));
|
|
1112
|
+
if (vals.some((v) => /^issue$/i.test(v))) {
|
|
1113
|
+
const id = vals.find((v) => UUID.test(v));
|
|
1114
|
+
return id ? { issue: id } : { research: "issue" };
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1021
1117
|
const out = {};
|
|
1022
1118
|
const loose = [];
|
|
1023
1119
|
for (const [k, raw] of Object.entries(args || {})) {
|
|
@@ -1130,10 +1226,18 @@ const COMMANDS = {
|
|
|
1130
1226
|
},
|
|
1131
1227
|
// 리서치 Report 의 발견 하나를 고친다 — 대시보드 Report 의 "Fix with your agent" 명령
|
|
1132
1228
|
fix: {
|
|
1133
|
-
description: "Fix a finding from a Usercase research report in this repository's code",
|
|
1134
|
-
argsSchema: {
|
|
1135
|
-
|
|
1136
|
-
|
|
1229
|
+
description: "Fix a finding from a Usercase research report, or an issue from Issues, in this repository's code",
|
|
1230
|
+
argsSchema: {
|
|
1231
|
+
research: z.string().optional(),
|
|
1232
|
+
finding: z.string().optional(),
|
|
1233
|
+
audience: z.string().optional(),
|
|
1234
|
+
issue: z.string().optional().describe("An issue id from the dashboard's Issues — fixes that issue instead of a report finding"),
|
|
1235
|
+
},
|
|
1236
|
+
body: ({ research, finding, audience, issue }) =>
|
|
1237
|
+
// Issues 의 문제 — `fix issue <id>`. 이름 없이 issue 만 오면 목록부터
|
|
1238
|
+
issue || String(research || "").trim().toLowerCase() === "issue"
|
|
1239
|
+
? agentDoc("fix-issue.md", { issue: String(issue || "").trim() || "(not given — list the open issues and ask)", research_cmd: cmd("research") })
|
|
1240
|
+
: agentDoc("fix.md", {
|
|
1137
1241
|
research: research || "(ask the user — the dashboard Report shows the command)",
|
|
1138
1242
|
finding: finding ? String(finding).toUpperCase() : "(not given — list the findings and ask)",
|
|
1139
1243
|
audience: audience === "real" ? "real (the report of real people)" : "virtual (the report of virtual users)",
|
|
@@ -1155,7 +1259,7 @@ if (PLUGIN || CODEX || OTHER) {
|
|
|
1155
1259
|
"instructions",
|
|
1156
1260
|
{
|
|
1157
1261
|
description:
|
|
1158
|
-
"The step-by-step instructions for a Usercase command (research, install, path, question, new-research, steps, connect, fix). The plugin's /usercase:* commands call this; follow what it returns as the user's request.",
|
|
1262
|
+
"The step-by-step instructions for a Usercase command (research, install, path, question, new-research, steps, connect, fix — a report finding, or an issue with `issue`). The plugin's /usercase:* commands call this; follow what it returns as the user's request.",
|
|
1159
1263
|
inputSchema: {
|
|
1160
1264
|
command: z.enum(Object.keys(COMMANDS)),
|
|
1161
1265
|
...COMMANDS.research.argsSchema,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@usercase/mcp",
|
|
3
|
-
"version": "0.2.11-next.
|
|
3
|
+
"version": "0.2.11-next.4",
|
|
4
4
|
"description": "Virtual users that browse your website and tell you what they thought — an MCP server for Claude Code",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"homepage": "https://usercase.io",
|
package/src/browser/study.js
CHANGED
|
@@ -226,11 +226,68 @@ async function movePanel(page, away) {
|
|
|
226
226
|
await drag(tx, ty);
|
|
227
227
|
}
|
|
228
228
|
|
|
229
|
-
/**
|
|
229
|
+
/**
|
|
230
|
+
* 패널 안 요소 위에 페이지의 다른 무언가가 올라와 있나 — 그 요소 한가운데 점의 맨 위 요소로 본다.
|
|
231
|
+
* 고객 사이트의 설문 팝업 · 채팅 · 쿠키 배너가 z-index 최대값으로 패널을 덮어 클릭을 가져갔다(11/05 bluetags Maze).
|
|
232
|
+
* 특정 위젯 이름을 정해 두지 않는다 — 덮은 것의 모양(태그 · 주소의 도메인 · 글 · 크기)으로 설명한다.
|
|
233
|
+
* @returns 안 덮였으면 null, 덮였으면 { at: {x, y} 덮인 점, what: 사람이 읽을 설명 }
|
|
234
|
+
*/
|
|
235
|
+
async function coveredBy(locator) {
|
|
236
|
+
return locator
|
|
237
|
+
.first()
|
|
238
|
+
.evaluate((el) => {
|
|
239
|
+
const r = el.getBoundingClientRect();
|
|
240
|
+
if (!r.width || !r.height) return null;
|
|
241
|
+
const x = r.x + r.width / 2;
|
|
242
|
+
const y = r.y + r.height / 2;
|
|
243
|
+
const top = document.elementFromPoint(x, y);
|
|
244
|
+
const host = document.getElementById("eb-plugin");
|
|
245
|
+
if (!top || top === host || (host && host.contains(top))) return null;
|
|
246
|
+
// 덮은 덩어리 — 위로 올라가 떠 있는(fixed · absolute) 상자까지
|
|
247
|
+
let box = top;
|
|
248
|
+
for (let i = 0; i < 8 && box.parentElement && box.parentElement !== document.body; i++) {
|
|
249
|
+
const st = getComputedStyle(box);
|
|
250
|
+
if (st.position === "fixed" || st.position === "absolute") break;
|
|
251
|
+
box = box.parentElement;
|
|
252
|
+
}
|
|
253
|
+
const br = box.getBoundingClientRect();
|
|
254
|
+
const tag = box.tagName.toLowerCase();
|
|
255
|
+
let from = "";
|
|
256
|
+
if (tag === "iframe") {
|
|
257
|
+
try {
|
|
258
|
+
from = new URL(box.src).host;
|
|
259
|
+
} catch (e) {}
|
|
260
|
+
}
|
|
261
|
+
const words = (tag === "iframe" ? box.getAttribute("title") || "" : box.innerText || "").replace(/\s+/g, " ").trim().slice(0, 70);
|
|
262
|
+
const kind = tag === "iframe" ? "an embedded frame" + (from ? " from " + from : "") : "a box";
|
|
263
|
+
const what = `${kind}${words ? ` showing "${words}"` : ""}, ${Math.round(br.width)}×${Math.round(br.height)} px`;
|
|
264
|
+
return { at: { x: Math.round(x), y: Math.round(y) }, what };
|
|
265
|
+
})
|
|
266
|
+
.catch(() => null);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* 패널 안 무언가를 누른다. 다른 무언가가 덮고 있으면 패널을 그 반대쪽으로 끌어 비키고 다시 본다 — 사람이 하듯이.
|
|
271
|
+
* 그래도 덮이면 던진다(조용히 넘기지 않는다 — 넘기면 방문자는 눌렀다고 믿고 같은 창에 갇힌다).
|
|
272
|
+
* @returns 비켜서 눌렀으면 덮었던 것의 설명, 아니면 null
|
|
273
|
+
*/
|
|
274
|
+
async function clickInPanel(page, locator) {
|
|
275
|
+
const cover = await coveredBy(locator);
|
|
276
|
+
if (cover) {
|
|
277
|
+
await movePanel(page, cover.at).catch(() => {});
|
|
278
|
+
await sleep(300);
|
|
279
|
+
const still = await coveredBy(locator);
|
|
280
|
+
if (still) throw new Error(`something else on the page is on top of it — ${still.what} — so the click does not reach the study panel`);
|
|
281
|
+
}
|
|
282
|
+
await locator.first().click({ timeout: 5000 });
|
|
283
|
+
return cover ? cover.what : null;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** 위젯 단추를 누른다. @returns 덮고 있던 것을 비켜서 눌렀으면 그 설명 */
|
|
230
287
|
async function press(page, key) {
|
|
231
288
|
const sel = PRESS[key];
|
|
232
289
|
if (!sel) throw new Error(`Unknown study button ${key}`);
|
|
233
|
-
|
|
290
|
+
return clickInPanel(page, page.locator(sel));
|
|
234
291
|
}
|
|
235
292
|
|
|
236
293
|
/**
|
|
@@ -242,11 +299,11 @@ async function answerQuestion(page, q, answer) {
|
|
|
242
299
|
const root = page.locator("#eb-plugin .eb-frost-survey");
|
|
243
300
|
if (Number.isInteger(answer.index) && answer.index >= 0) {
|
|
244
301
|
const sel = q.kind === "choice" ? ".eb-sq-choice:not(:has(input))" : q.kind === "scale" ? ".eb-sq-likert-btn" : ".eb-sq-yesno-btn";
|
|
245
|
-
await root.locator(sel).nth(answer.index)
|
|
302
|
+
await clickInPanel(page, root.locator(sel).nth(answer.index));
|
|
246
303
|
} else if (answer.text) {
|
|
247
304
|
const box = q.kind === "choice" ? root.locator("input.eb-sq-choice-other") : root.locator("textarea.eb-sq-textarea, input.eb-sq-input").first();
|
|
248
305
|
if (!(await box.count())) return "there is no box to type into for this question — pick one of the options";
|
|
249
|
-
await
|
|
306
|
+
await clickInPanel(page, box);
|
|
250
307
|
await box.fill(String(answer.text).slice(0, q.multiline ? 1000 : 200), { timeout: 5000 });
|
|
251
308
|
}
|
|
252
309
|
await sleep(400);
|
|
@@ -257,4 +314,4 @@ async function answerQuestion(page, q, answer) {
|
|
|
257
314
|
return null;
|
|
258
315
|
}
|
|
259
316
|
|
|
260
|
-
module.exports = { readStudy, waitState, join, press, movePanel, answerQuestion, isSurveyUrl };
|
|
317
|
+
module.exports = { readStudy, waitState, join, press, movePanel, answerQuestion, isSurveyUrl, coveredBy };
|