cobro-mcp 0.10.2 → 0.10.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/CHANGELOG.md +7 -0
- package/README.ko.md +2 -2
- package/README.md +2 -2
- package/dist/overlay.js +60 -23
- package/dist/server.js +16 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,13 @@ All notable changes to this project are documented here. The format follows [Kee
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.10.3] — 2026-10-01
|
|
8
|
+
### Fixed
|
|
9
|
+
- Markers and the working outline no longer jump to a neighbouring element with different text when the picked element is removed (e.g. a deleted table column); the element is reported as missing on Send. Elements without text (`col`, empty cells, icons) can still be matched to a neighbour.
|
|
10
|
+
- `open` on a localhost URL that is refused or returns an empty response now suggests trying 127.0.0.1 (localhost may resolve to another process over IPv6).
|
|
11
|
+
- The first letter of the toolbar status text is no longer clipped next to the status chip while the text fades in.
|
|
12
|
+
- Vue: picking an element inside a component-library component (Element Plus, Vuetify, …) now reports the SFC in your code that uses it, plus up to two calling SFCs, instead of only the library component name.
|
|
13
|
+
|
|
7
14
|
## [0.10.2] — 2026-10-01
|
|
8
15
|
### Added
|
|
9
16
|
- `done` now returns a `next` hint telling the agent to call wait() again once every received batch is done.
|
package/README.ko.md
CHANGED
|
@@ -144,8 +144,8 @@ claude mcp add -s user cobro -- npx -y cobro-mcp@latest
|
|
|
144
144
|
| `elements[].rect` | `{ x, y, w, h }` 페이지 좌표(스크롤 포함), 정수 |
|
|
145
145
|
| `elements[].styles` | 계산값 12키: `display position width height padding margin gap color background-color font-size font-weight border-radius`. `none`/`normal`은 생략하되 `display: none`은 "안 보임" 단서로 남긴다 |
|
|
146
146
|
| `elements[].react` | React dev 빌드에서만 `{ component, source?, callers? }`. 아니면 키 자체가 없다. `source`는 소스맵이 있는 dev 서버(React 19) 또는 React 18까지에서만 온다. `source`는 찍은 요소 자신의 JSX 줄(사용자 코드일 때). 라이브러리가 그린 요소면 사용자 코드에서 가장 가까운 호출 지점. `callers`는 `source` 위의 사용자 코드 호출 지점(가까운 순, 최대 2). `source`가 한 줄 패스스루 래퍼면 보통 첫 caller가 진짜 고칠 자리. Vite·Next.js(Turbopack) dev 서버에서 동작 |
|
|
147
|
-
| `elements[].vue` | Vue 3 dev 빌드에서 `{ component, source? }`. `source`는 SFC 경로만(행 없음). 아니면 키 생략 |
|
|
148
|
-
| `elements[].missing` |
|
|
147
|
+
| `elements[].vue` | Vue 3 dev 빌드에서 `{ component, source?, callers? }`. `source`는 SFC 경로만(행 없음). `source`는 항상 사용자 코드의 SFC. 요소가 컴포넌트 라이브러리(Element Plus·Vuetify 등) 컴포넌트 안에서 그려지면 `component`는 그 SFC 템플릿에 직접 쓴 라이브러리 컴포넌트 이름(템플릿에서 검색 가능), 아니면 SFC 자신의 이름. `callers`는 `source` 위의 SFC(가까운 순, 최대 2). 아니면 키 생략 |
|
|
148
|
+
| `elements[].missing` | 선택자로 다시 찾았을 때 같은 요소로 확인되지 않으면 `true`(삭제되었거나 tag·text가 바뀜) — 나머지 값은 찍은 시점 그대로 |
|
|
149
149
|
| `elements[].ref` | 패널 행의 안정 번호(`"2"`, 영역 `"1"` 안의 요소면 `"1a"`). 메모가 이 번호를 가리킨다. 초안이 사는 동안 재번호 없음 |
|
|
150
150
|
| `regions[].ref` | `elements[].ref`와 같은 체계를 공유하는 안정 번호 |
|
|
151
151
|
| `batches[].regions[]` | 사용자가 드래그로 그린 사각형: 그룹(안의 요소가 이 영역의 ref를 접두사로 갖는 ref를 받음)이거나 빈 곳 위 영역(요소 없음). `rect`는 페이지 좌표, `within`은 감싸는 요소의 선택자. 없으면 키 생략 |
|
package/README.md
CHANGED
|
@@ -144,8 +144,8 @@ If `wait` returns `pending`, call it again (not an error). If `browserGone: true
|
|
|
144
144
|
| `elements[].rect` | `{ x, y, w, h }` in page coordinates (scroll included), integers |
|
|
145
145
|
| `elements[].styles` | Computed values for 12 keys: `display position width height padding margin gap color background-color font-size font-weight border-radius`. `none`/`normal` are dropped, except `display: none` which is kept as a "not visible" clue |
|
|
146
146
|
| `elements[].react` | `{ component, source?, callers? }` from React dev builds only; key omitted otherwise. `source` needs a dev server with source maps (React 19) or React ≤ 18. `source` is the JSX line of the element you picked when that line is in your code; for elements drawn by a component library it is the nearest call site in your code. `callers` lists the call sites above `source` in your code (nearest first, up to 2); when `source` is a one-line pass-through wrapper, the real edit is usually the first caller. Works with Vite and Next.js (Turbopack) dev servers. |
|
|
147
|
-
| `elements[].vue` | `{ component, source? }` from Vue 3 dev builds; `source` is the SFC path only (no line). Omitted otherwise |
|
|
148
|
-
| `elements[].missing` | `true` when the selector
|
|
147
|
+
| `elements[].vue` | `{ component, source?, callers? }` from Vue 3 dev builds; `source` is the SFC path only (no line). `source` is always an SFC in your code. When the element is drawn by a component-library component (Element Plus, Vuetify, …), `component` is the library component you wrote in that SFC's template (its name is searchable there) and `source` is that SFC; otherwise `component` is the SFC's own name. `callers` lists the SFCs above `source` (nearest first, up to 2). Omitted otherwise |
|
|
148
|
+
| `elements[].missing` | `true` when re-finding the element by its selector does not confirm it is the same element (it was removed, or its tag or text changed) — the other values are as captured |
|
|
149
149
|
| `elements[].ref` | Stable number of the row in the panel (`"2"`, or `"1a"` for an element inside region `"1"`); notes refer to it. Never renumbered while the draft lives |
|
|
150
150
|
| `regions[].ref` | Same stable numbering, shared with `elements[].ref` |
|
|
151
151
|
| `batches[].regions[]` | Rectangles the user dragged: either a group (elements inside carry refs prefixed with the region's ref) or an area on empty space (no elements). `rect` in page coordinates, `within` = selector of the enclosing element. Omitted when none |
|
package/dist/overlay.js
CHANGED
|
@@ -25,6 +25,24 @@
|
|
|
25
25
|
// src/overlay/refs.ts
|
|
26
26
|
var isChildRef = (ref, g) => !!ref && new RegExp("^" + g + "[a-l]$").test(ref);
|
|
27
27
|
|
|
28
|
+
// src/overlay/resolve.ts
|
|
29
|
+
var normalizeText = (s) => (s || "").trim().replace(/\s+/g, " ").slice(0, 40);
|
|
30
|
+
function resolveEl(e) {
|
|
31
|
+
let found = null;
|
|
32
|
+
try {
|
|
33
|
+
found = document.querySelector(e.selector);
|
|
34
|
+
} catch {
|
|
35
|
+
}
|
|
36
|
+
if (!found || found.tagName.toLowerCase() !== e.tag) return null;
|
|
37
|
+
if (e.text && normalizeText(found.textContent) !== e.text) return null;
|
|
38
|
+
return found;
|
|
39
|
+
}
|
|
40
|
+
function groupAlive(kids) {
|
|
41
|
+
if (kids.length === 0) return true;
|
|
42
|
+
const verifiable = kids.filter((k) => k.text);
|
|
43
|
+
return (verifiable.length > 0 ? verifiable : kids).some((k) => resolveEl(k) !== null);
|
|
44
|
+
}
|
|
45
|
+
|
|
28
46
|
// src/core/page.ts
|
|
29
47
|
function samePage(a, b) {
|
|
30
48
|
try {
|
|
@@ -170,7 +188,7 @@ textarea:disabled{opacity:.6;cursor:not-allowed}
|
|
|
170
188
|
.status-in.scroll{max-width:none;overflow:visible;text-overflow:clip}
|
|
171
189
|
.status-in.enter{animation:cobroin .15s ease-out}
|
|
172
190
|
.status-in a.goto{color:inherit;text-decoration:underline;cursor:pointer}
|
|
173
|
-
@keyframes cobroin{from{opacity:.4
|
|
191
|
+
@keyframes cobroin{from{opacity:.4}to{opacity:1}}
|
|
174
192
|
@media (prefers-reduced-motion: reduce){.status-in{transition:none}.status-in.enter{animation:none}}
|
|
175
193
|
.wbox{position:fixed;border:1.5px solid var(--info);border-radius:2px;pointer-events:none;box-sizing:border-box;animation:cobrobreathe 2.4s ease-in-out infinite}
|
|
176
194
|
.wbox.region{border-style:dashed;background:color-mix(in srgb, var(--info) 6%, transparent)}
|
|
@@ -930,16 +948,13 @@ div.queue{display:flex;flex-direction:column;gap:8px;max-height:min(45vh,320px);
|
|
|
930
948
|
const isChild = /[a-z]$/.test(e.ref ?? "");
|
|
931
949
|
const groupRef = isChild ? e.ref.slice(0, -1) : null;
|
|
932
950
|
if (isChild && vm.expanded !== groupRef) return;
|
|
933
|
-
|
|
934
|
-
try {
|
|
935
|
-
target = document.querySelector(e.selector);
|
|
936
|
-
} catch {
|
|
937
|
-
}
|
|
951
|
+
const target = resolveEl(e);
|
|
938
952
|
if (!target) return;
|
|
939
953
|
const r = target.getBoundingClientRect();
|
|
940
954
|
place(el("div", isChild ? "marker child" : "marker"), r.left - 2, r.top - 2, r.width + 4, r.height + 4, e.ref ?? "");
|
|
941
955
|
});
|
|
942
956
|
cur.regions?.forEach((r) => {
|
|
957
|
+
if (!groupAlive(cur.elements.filter((e) => isChildRef(e.ref, r.ref ?? "")))) return;
|
|
943
958
|
place(el("div", "marker region"), r.rect.x - scrollX, r.rect.y - scrollY, r.rect.w, r.rect.h, r.ref ?? "");
|
|
944
959
|
});
|
|
945
960
|
}
|
|
@@ -959,16 +974,13 @@ div.queue{display:flex;flex-direction:column;gap:8px;max-height:min(45vh,320px);
|
|
|
959
974
|
b.elements.forEach((e) => {
|
|
960
975
|
if (e.missing) return;
|
|
961
976
|
if (/[a-z]$/.test(e.ref ?? "")) return;
|
|
962
|
-
|
|
963
|
-
try {
|
|
964
|
-
target = document.querySelector(e.selector);
|
|
965
|
-
} catch {
|
|
966
|
-
}
|
|
977
|
+
const target = resolveEl(e);
|
|
967
978
|
if (!target) return;
|
|
968
979
|
const r = target.getBoundingClientRect();
|
|
969
980
|
place(wbox("wbox"), r.left - 2, r.top - 2, r.width + 4, r.height + 4, e.ref ?? "");
|
|
970
981
|
});
|
|
971
982
|
b.regions?.forEach((r) => {
|
|
983
|
+
if (!groupAlive(b.elements.filter((e) => isChildRef(e.ref, r.ref ?? "")))) return;
|
|
972
984
|
place(wbox("wbox region"), r.rect.x - scrollX, r.rect.y - scrollY, r.rect.w, r.rect.h, r.ref ?? "");
|
|
973
985
|
});
|
|
974
986
|
}
|
|
@@ -1325,17 +1337,43 @@ ${Math.round(r.width)}\xD7${Math.round(r.height)}`;
|
|
|
1325
1337
|
const segs = norm.split("/").filter(Boolean);
|
|
1326
1338
|
return segs.slice(-2).join("/");
|
|
1327
1339
|
}
|
|
1340
|
+
function fileBase(file) {
|
|
1341
|
+
return (file.replace(/\\/g, "/").split("/").pop() ?? "").replace(/\.[^.]+$/, "");
|
|
1342
|
+
}
|
|
1343
|
+
var BUILTIN = /* @__PURE__ */ new Set(["BaseTransition", "KeepAlive", "TransitionGroup"]);
|
|
1328
1344
|
function vueInfo(el, root) {
|
|
1329
1345
|
let c = el["__vueParentComponent"] ?? null;
|
|
1330
|
-
|
|
1346
|
+
let fallback;
|
|
1347
|
+
let fallbackBuiltin;
|
|
1348
|
+
let libName;
|
|
1349
|
+
let hit;
|
|
1350
|
+
const callers = [];
|
|
1351
|
+
for (let i = 0; c && i < 15 && callers.length < 2; i++, c = c.parent ?? null) {
|
|
1331
1352
|
const name = c.type?.name || c.type?.__name;
|
|
1353
|
+
const builtin = !!name && BUILTIN.has(name);
|
|
1332
1354
|
if (name) {
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
return source ? { component: name, source } : { component: name };
|
|
1355
|
+
if (builtin) fallbackBuiltin ??= name;
|
|
1356
|
+
else fallback ??= name;
|
|
1336
1357
|
}
|
|
1358
|
+
if (builtin) continue;
|
|
1359
|
+
const file = c.type?.__file;
|
|
1360
|
+
const source = file ? normalizeVueSource(file, root) : void 0;
|
|
1361
|
+
if (!source) {
|
|
1362
|
+
if (!hit) {
|
|
1363
|
+
const n = name || (file ? fileBase(file) : "");
|
|
1364
|
+
if (n) libName = n;
|
|
1365
|
+
}
|
|
1366
|
+
continue;
|
|
1367
|
+
}
|
|
1368
|
+
if (!hit) hit = { component: libName ?? (name || fileBase(file)), source };
|
|
1369
|
+
else if (source !== hit.source && !callers.includes(source)) callers.push(source);
|
|
1337
1370
|
}
|
|
1338
|
-
|
|
1371
|
+
if (!hit) {
|
|
1372
|
+
const n = fallback ?? fallbackBuiltin;
|
|
1373
|
+
return n ? { component: n } : void 0;
|
|
1374
|
+
}
|
|
1375
|
+
if (callers.length) hit.callers = callers;
|
|
1376
|
+
return hit;
|
|
1339
1377
|
}
|
|
1340
1378
|
var vue = { key: "vue", detect: vueInfo };
|
|
1341
1379
|
|
|
@@ -1412,7 +1450,7 @@ ${Math.round(r.width)}\xD7${Math.round(r.height)}`;
|
|
|
1412
1450
|
selector: uniqueSelector(el),
|
|
1413
1451
|
tag: el.tagName.toLowerCase(),
|
|
1414
1452
|
classes: [...el.classList],
|
|
1415
|
-
text: (el.textContent
|
|
1453
|
+
text: normalizeText(el.textContent),
|
|
1416
1454
|
rect: { x: Math.round(r.left + window.scrollX), y: Math.round(r.top + window.scrollY), w: Math.round(r.width), h: Math.round(r.height) },
|
|
1417
1455
|
styles
|
|
1418
1456
|
};
|
|
@@ -1597,12 +1635,7 @@ ${Math.round(r.width)}\xD7${Math.round(r.height)}`;
|
|
|
1597
1635
|
const resolveDraft = (b) => {
|
|
1598
1636
|
ensureRefs(b);
|
|
1599
1637
|
return { ...b, status: "draft", elements: b.elements.map((e, i) => {
|
|
1600
|
-
|
|
1601
|
-
try {
|
|
1602
|
-
found = document.querySelector(e.selector);
|
|
1603
|
-
} catch {
|
|
1604
|
-
}
|
|
1605
|
-
const missing = !found;
|
|
1638
|
+
const missing = !resolveEl(e);
|
|
1606
1639
|
if (missing !== !!e.missing) chan.send({ type: "resolved", batchId: b.id, index: i, missing });
|
|
1607
1640
|
return { ...e, missing };
|
|
1608
1641
|
}) };
|
|
@@ -1647,6 +1680,10 @@ ${Math.round(r.width)}\xD7${Math.round(r.height)}`;
|
|
|
1647
1680
|
ui.focusNote();
|
|
1648
1681
|
return;
|
|
1649
1682
|
}
|
|
1683
|
+
for (const b of ready) for (const e of b.elements) {
|
|
1684
|
+
const missing = !resolveEl(e);
|
|
1685
|
+
if (missing !== !!e.missing) e.missing = missing;
|
|
1686
|
+
}
|
|
1650
1687
|
flushDraft();
|
|
1651
1688
|
chan.send({ type: "send", batchIds: ready.map((b) => b.id), page: pageInfo() });
|
|
1652
1689
|
drafts = (drafts ?? []).filter((b) => !ready.includes(b));
|
package/dist/server.js
CHANGED
|
@@ -824,6 +824,17 @@ async function fetchTextGuarded(request, url, maxBytes = 8 * 1024 * 1024, timeou
|
|
|
824
824
|
function classifyLaunchFailure(tried, lockExists) {
|
|
825
825
|
return tried.every((t) => t.includes("has been closed")) && lockExists ? "profile-locked" : "not-found";
|
|
826
826
|
}
|
|
827
|
+
var LOCALHOST_FAILURE = /ERR_EMPTY_RESPONSE|ERR_CONNECTION_REFUSED|NS_ERROR_CONNECTION_REFUSED|Could not connect to server|Server returned nothing/;
|
|
828
|
+
function withLocalhostHint(url, message) {
|
|
829
|
+
let host = "";
|
|
830
|
+
try {
|
|
831
|
+
host = new URL(url).hostname;
|
|
832
|
+
} catch {
|
|
833
|
+
return message;
|
|
834
|
+
}
|
|
835
|
+
if (host !== "localhost" || !LOCALHOST_FAILURE.test(message)) return message;
|
|
836
|
+
return `${message} \u2014 localhost may resolve to another process (IPv6 ::1); try 127.0.0.1 instead.`;
|
|
837
|
+
}
|
|
827
838
|
function parseEngine(v) {
|
|
828
839
|
return v === "webkit" || v === "firefox" || v === "chromium" ? v : "chromium";
|
|
829
840
|
}
|
|
@@ -936,7 +947,11 @@ Close the other session's cobro browser (close), or set COBRO_PROFILE_DIR to a d
|
|
|
936
947
|
await this.launch();
|
|
937
948
|
this.launchedOnce = true;
|
|
938
949
|
}
|
|
939
|
-
|
|
950
|
+
try {
|
|
951
|
+
await this.page.goto(url, { waitUntil: "load" });
|
|
952
|
+
} catch (e) {
|
|
953
|
+
throw new Error(withLocalhostHint(url, e.message), { cause: e });
|
|
954
|
+
}
|
|
940
955
|
return { title: await this.page.title(), restarted };
|
|
941
956
|
}
|
|
942
957
|
async rectOfSelector(selector) {
|
package/package.json
CHANGED