@jobshimo/browser-link 0.4.1 → 0.5.1
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/LICENSE +21 -21
- package/README.md +139 -139
- package/dist/bridge/server.d.ts +6 -0
- package/dist/bridge/server.js +2 -1
- package/dist/bridge/server.js.map +1 -1
- package/dist/cli.js +73 -73
- package/dist/commands/free-port.js +8 -2
- package/dist/commands/free-port.js.map +1 -1
- package/dist/extension/background.js +90 -90
- package/dist/extension/icons/icon.svg +14 -14
- package/dist/extension/manifest.json +28 -28
- package/dist/extension/popup.html +88 -88
- package/dist/map/db.js +28 -28
- package/dist/map/queries.js +4 -4
- package/dist/tools/server-instructions.js +50 -50
- package/package.json +70 -70
|
@@ -96,82 +96,82 @@ function attachDebuggerListener(state) {
|
|
|
96
96
|
};
|
|
97
97
|
state.debuggerListener = listener;
|
|
98
98
|
}
|
|
99
|
-
const SNAPSHOT_JS = `
|
|
100
|
-
(() => {
|
|
101
|
-
function isVisible(el) {
|
|
102
|
-
if (!(el instanceof HTMLElement)) return true;
|
|
103
|
-
const style = getComputedStyle(el);
|
|
104
|
-
if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') return false;
|
|
105
|
-
if (el.offsetParent === null && style.position !== 'fixed') return false;
|
|
106
|
-
return true;
|
|
107
|
-
}
|
|
108
|
-
function shortText(el) {
|
|
109
|
-
const t = (el.innerText || el.textContent || '').trim();
|
|
110
|
-
return t.length > 120 ? t.slice(0, 120) + '...' : t;
|
|
111
|
-
}
|
|
112
|
-
function safeCss(s) {
|
|
113
|
-
return s.replace(/"/g, '\\\\"');
|
|
114
|
-
}
|
|
115
|
-
function genSelector(el) {
|
|
116
|
-
if (el.id && !/^[\\d]/.test(el.id) && !/\\s/.test(el.id)) {
|
|
117
|
-
try { if (document.querySelectorAll('#' + CSS.escape(el.id)).length === 1) return '#' + CSS.escape(el.id); } catch (_) {}
|
|
118
|
-
}
|
|
119
|
-
const tid = el.getAttribute('data-testid');
|
|
120
|
-
if (tid) return '[data-testid="' + safeCss(tid) + '"]';
|
|
121
|
-
const al = el.getAttribute('aria-label');
|
|
122
|
-
if (al && al.length < 60) return el.tagName.toLowerCase() + '[aria-label="' + safeCss(al) + '"]';
|
|
123
|
-
const name = el.getAttribute('name');
|
|
124
|
-
if (name && (el.tagName === 'INPUT' || el.tagName === 'SELECT' || el.tagName === 'TEXTAREA')) {
|
|
125
|
-
return el.tagName.toLowerCase() + '[name="' + safeCss(name) + '"]';
|
|
126
|
-
}
|
|
127
|
-
const parts = [];
|
|
128
|
-
let cur = el;
|
|
129
|
-
while (cur && cur.nodeType === 1 && cur !== document.body && parts.length < 6) {
|
|
130
|
-
let part = cur.tagName.toLowerCase();
|
|
131
|
-
const parent = cur.parentElement;
|
|
132
|
-
if (parent) {
|
|
133
|
-
const sib = Array.from(parent.children).filter(s => s.tagName === cur.tagName);
|
|
134
|
-
if (sib.length > 1) {
|
|
135
|
-
part += ':nth-of-type(' + (sib.indexOf(cur) + 1) + ')';
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
parts.unshift(part);
|
|
139
|
-
cur = parent;
|
|
140
|
-
}
|
|
141
|
-
return parts.join(' > ');
|
|
142
|
-
}
|
|
143
|
-
const sel = 'a[href], button, input, select, textarea, [role=button], [role=link], [role=checkbox], [role=tab], [role=menuitem], [contenteditable=true]';
|
|
144
|
-
const interactive = [];
|
|
145
|
-
document.querySelectorAll(sel).forEach((el) => {
|
|
146
|
-
if (!isVisible(el)) return;
|
|
147
|
-
interactive.push({
|
|
148
|
-
tag: el.tagName.toLowerCase(),
|
|
149
|
-
role: el.getAttribute('role') || el.tagName.toLowerCase(),
|
|
150
|
-
text: shortText(el),
|
|
151
|
-
value: 'value' in el ? (el.value || '') : '',
|
|
152
|
-
placeholder: el.getAttribute('placeholder') || '',
|
|
153
|
-
aria_label: el.getAttribute('aria-label') || '',
|
|
154
|
-
name: el.getAttribute('name') || '',
|
|
155
|
-
type: el.getAttribute('type') || '',
|
|
156
|
-
href: el.getAttribute('href') || '',
|
|
157
|
-
disabled: 'disabled' in el ? !!el.disabled : false,
|
|
158
|
-
selector: genSelector(el),
|
|
159
|
-
});
|
|
160
|
-
});
|
|
161
|
-
const headings = [];
|
|
162
|
-
document.querySelectorAll('h1, h2, h3').forEach((h) => {
|
|
163
|
-
if (!isVisible(h)) return;
|
|
164
|
-
headings.push({ level: h.tagName, text: shortText(h) });
|
|
165
|
-
});
|
|
166
|
-
const visibleText = (document.body && document.body.innerText) ? document.body.innerText.slice(0, 4000) : '';
|
|
167
|
-
return {
|
|
168
|
-
title: document.title,
|
|
169
|
-
url: location.href,
|
|
170
|
-
headings: headings.slice(0, 30),
|
|
171
|
-
text: visibleText,
|
|
172
|
-
interactive: interactive.slice(0, 120),
|
|
173
|
-
};
|
|
174
|
-
})()
|
|
99
|
+
const SNAPSHOT_JS = `
|
|
100
|
+
(() => {
|
|
101
|
+
function isVisible(el) {
|
|
102
|
+
if (!(el instanceof HTMLElement)) return true;
|
|
103
|
+
const style = getComputedStyle(el);
|
|
104
|
+
if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') return false;
|
|
105
|
+
if (el.offsetParent === null && style.position !== 'fixed') return false;
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
function shortText(el) {
|
|
109
|
+
const t = (el.innerText || el.textContent || '').trim();
|
|
110
|
+
return t.length > 120 ? t.slice(0, 120) + '...' : t;
|
|
111
|
+
}
|
|
112
|
+
function safeCss(s) {
|
|
113
|
+
return s.replace(/"/g, '\\\\"');
|
|
114
|
+
}
|
|
115
|
+
function genSelector(el) {
|
|
116
|
+
if (el.id && !/^[\\d]/.test(el.id) && !/\\s/.test(el.id)) {
|
|
117
|
+
try { if (document.querySelectorAll('#' + CSS.escape(el.id)).length === 1) return '#' + CSS.escape(el.id); } catch (_) {}
|
|
118
|
+
}
|
|
119
|
+
const tid = el.getAttribute('data-testid');
|
|
120
|
+
if (tid) return '[data-testid="' + safeCss(tid) + '"]';
|
|
121
|
+
const al = el.getAttribute('aria-label');
|
|
122
|
+
if (al && al.length < 60) return el.tagName.toLowerCase() + '[aria-label="' + safeCss(al) + '"]';
|
|
123
|
+
const name = el.getAttribute('name');
|
|
124
|
+
if (name && (el.tagName === 'INPUT' || el.tagName === 'SELECT' || el.tagName === 'TEXTAREA')) {
|
|
125
|
+
return el.tagName.toLowerCase() + '[name="' + safeCss(name) + '"]';
|
|
126
|
+
}
|
|
127
|
+
const parts = [];
|
|
128
|
+
let cur = el;
|
|
129
|
+
while (cur && cur.nodeType === 1 && cur !== document.body && parts.length < 6) {
|
|
130
|
+
let part = cur.tagName.toLowerCase();
|
|
131
|
+
const parent = cur.parentElement;
|
|
132
|
+
if (parent) {
|
|
133
|
+
const sib = Array.from(parent.children).filter(s => s.tagName === cur.tagName);
|
|
134
|
+
if (sib.length > 1) {
|
|
135
|
+
part += ':nth-of-type(' + (sib.indexOf(cur) + 1) + ')';
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
parts.unshift(part);
|
|
139
|
+
cur = parent;
|
|
140
|
+
}
|
|
141
|
+
return parts.join(' > ');
|
|
142
|
+
}
|
|
143
|
+
const sel = 'a[href], button, input, select, textarea, [role=button], [role=link], [role=checkbox], [role=tab], [role=menuitem], [contenteditable=true]';
|
|
144
|
+
const interactive = [];
|
|
145
|
+
document.querySelectorAll(sel).forEach((el) => {
|
|
146
|
+
if (!isVisible(el)) return;
|
|
147
|
+
interactive.push({
|
|
148
|
+
tag: el.tagName.toLowerCase(),
|
|
149
|
+
role: el.getAttribute('role') || el.tagName.toLowerCase(),
|
|
150
|
+
text: shortText(el),
|
|
151
|
+
value: 'value' in el ? (el.value || '') : '',
|
|
152
|
+
placeholder: el.getAttribute('placeholder') || '',
|
|
153
|
+
aria_label: el.getAttribute('aria-label') || '',
|
|
154
|
+
name: el.getAttribute('name') || '',
|
|
155
|
+
type: el.getAttribute('type') || '',
|
|
156
|
+
href: el.getAttribute('href') || '',
|
|
157
|
+
disabled: 'disabled' in el ? !!el.disabled : false,
|
|
158
|
+
selector: genSelector(el),
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
const headings = [];
|
|
162
|
+
document.querySelectorAll('h1, h2, h3').forEach((h) => {
|
|
163
|
+
if (!isVisible(h)) return;
|
|
164
|
+
headings.push({ level: h.tagName, text: shortText(h) });
|
|
165
|
+
});
|
|
166
|
+
const visibleText = (document.body && document.body.innerText) ? document.body.innerText.slice(0, 4000) : '';
|
|
167
|
+
return {
|
|
168
|
+
title: document.title,
|
|
169
|
+
url: location.href,
|
|
170
|
+
headings: headings.slice(0, 30),
|
|
171
|
+
text: visibleText,
|
|
172
|
+
interactive: interactive.slice(0, 120),
|
|
173
|
+
};
|
|
174
|
+
})()
|
|
175
175
|
`;
|
|
176
176
|
async function evaluateInTab(tabId, expression) {
|
|
177
177
|
const result = (await cdp(tabId, 'Runtime.evaluate', {
|
|
@@ -255,13 +255,13 @@ async function handleTool(state, msg) {
|
|
|
255
255
|
}
|
|
256
256
|
case 'click': {
|
|
257
257
|
const selector = String(p.selector);
|
|
258
|
-
const expr = `
|
|
259
|
-
(() => {
|
|
260
|
-
const el = document.querySelector(${JSON.stringify(selector)});
|
|
261
|
-
if (!el) return null;
|
|
262
|
-
el.scrollIntoView({ block: 'center', inline: 'center' });
|
|
263
|
-
const r = el.getBoundingClientRect();
|
|
264
|
-
return { x: r.left + r.width / 2, y: r.top + r.height / 2, tag: el.tagName.toLowerCase() };
|
|
258
|
+
const expr = `
|
|
259
|
+
(() => {
|
|
260
|
+
const el = document.querySelector(${JSON.stringify(selector)});
|
|
261
|
+
if (!el) return null;
|
|
262
|
+
el.scrollIntoView({ block: 'center', inline: 'center' });
|
|
263
|
+
const r = el.getBoundingClientRect();
|
|
264
|
+
return { x: r.left + r.width / 2, y: r.top + r.height / 2, tag: el.tagName.toLowerCase() };
|
|
265
265
|
})()`;
|
|
266
266
|
const coords = (await evaluateInTab(tabId, expr));
|
|
267
267
|
if (!coords) {
|
|
@@ -302,13 +302,13 @@ async function handleTool(state, msg) {
|
|
|
302
302
|
const selector = String(p.selector);
|
|
303
303
|
const text = String(p.text);
|
|
304
304
|
const clear = !!p.clear;
|
|
305
|
-
const focusExpr = `
|
|
306
|
-
(() => {
|
|
307
|
-
const el = document.querySelector(${JSON.stringify(selector)});
|
|
308
|
-
if (!el) return false;
|
|
309
|
-
el.focus();
|
|
310
|
-
${clear ? "if ('value' in el) { el.value = ''; el.dispatchEvent(new Event('input', { bubbles: true })); }" : ''}
|
|
311
|
-
return true;
|
|
305
|
+
const focusExpr = `
|
|
306
|
+
(() => {
|
|
307
|
+
const el = document.querySelector(${JSON.stringify(selector)});
|
|
308
|
+
if (!el) return false;
|
|
309
|
+
el.focus();
|
|
310
|
+
${clear ? "if ('value' in el) { el.value = ''; el.dispatchEvent(new Event('input', { bubbles: true })); }" : ''}
|
|
311
|
+
return true;
|
|
312
312
|
})()`;
|
|
313
313
|
const focused = await evaluateInTab(tabId, focusExpr);
|
|
314
314
|
if (!focused) {
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
<svg viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
-
<defs>
|
|
3
|
-
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
|
|
4
|
-
<stop offset="0%" stop-color="#3b82f6" />
|
|
5
|
-
<stop offset="100%" stop-color="#1d4ed8" />
|
|
6
|
-
</linearGradient>
|
|
7
|
-
</defs>
|
|
8
|
-
<rect width="128" height="128" rx="26" fill="url(#bg)" />
|
|
9
|
-
<g fill="none" stroke="#ffffff" stroke-width="11" stroke-linecap="round" stroke-linejoin="round">
|
|
10
|
-
<path d="M52 78 L36 78 A22 22 0 0 1 36 34 L52 34" />
|
|
11
|
-
<path d="M76 34 L92 34 A22 22 0 0 1 92 78 L76 78" />
|
|
12
|
-
<line x1="44" y1="56" x2="84" y2="56" />
|
|
13
|
-
</g>
|
|
14
|
-
</svg>
|
|
1
|
+
<svg viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
|
|
4
|
+
<stop offset="0%" stop-color="#3b82f6" />
|
|
5
|
+
<stop offset="100%" stop-color="#1d4ed8" />
|
|
6
|
+
</linearGradient>
|
|
7
|
+
</defs>
|
|
8
|
+
<rect width="128" height="128" rx="26" fill="url(#bg)" />
|
|
9
|
+
<g fill="none" stroke="#ffffff" stroke-width="11" stroke-linecap="round" stroke-linejoin="round">
|
|
10
|
+
<path d="M52 78 L36 78 A22 22 0 0 1 36 34 L52 34" />
|
|
11
|
+
<path d="M76 34 L92 34 A22 22 0 0 1 92 78 L76 78" />
|
|
12
|
+
<line x1="44" y1="56" x2="84" y2="56" />
|
|
13
|
+
</g>
|
|
14
|
+
</svg>
|
|
@@ -1,28 +1,28 @@
|
|
|
1
|
-
{
|
|
2
|
-
"manifest_version": 3,
|
|
3
|
-
"name": "browser-link",
|
|
4
|
-
"version": "0.1
|
|
5
|
-
"description": "Bridge between Chrome and an MCP client (Claude Code
|
|
6
|
-
"permissions": ["debugger", "activeTab", "storage", "tabs"],
|
|
7
|
-
"host_permissions": [],
|
|
8
|
-
"icons": {
|
|
9
|
-
"16": "icons/icon-16.png",
|
|
10
|
-
"32": "icons/icon-32.png",
|
|
11
|
-
"48": "icons/icon-48.png",
|
|
12
|
-
"128": "icons/icon-128.png"
|
|
13
|
-
},
|
|
14
|
-
"background": {
|
|
15
|
-
"service_worker": "background.js",
|
|
16
|
-
"type": "module"
|
|
17
|
-
},
|
|
18
|
-
"action": {
|
|
19
|
-
"default_popup": "popup.html",
|
|
20
|
-
"default_title": "browser-link",
|
|
21
|
-
"default_icon": {
|
|
22
|
-
"16": "icons/icon-16.png",
|
|
23
|
-
"32": "icons/icon-32.png",
|
|
24
|
-
"48": "icons/icon-48.png",
|
|
25
|
-
"128": "icons/icon-128.png"
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"manifest_version": 3,
|
|
3
|
+
"name": "browser-link",
|
|
4
|
+
"version": "0.0.1",
|
|
5
|
+
"description": "Bridge between Chrome and an MCP client (Claude Code). Per-tab manual activation.",
|
|
6
|
+
"permissions": ["debugger", "activeTab", "storage", "tabs"],
|
|
7
|
+
"host_permissions": [],
|
|
8
|
+
"icons": {
|
|
9
|
+
"16": "icons/icon-16.png",
|
|
10
|
+
"32": "icons/icon-32.png",
|
|
11
|
+
"48": "icons/icon-48.png",
|
|
12
|
+
"128": "icons/icon-128.png"
|
|
13
|
+
},
|
|
14
|
+
"background": {
|
|
15
|
+
"service_worker": "background.js",
|
|
16
|
+
"type": "module"
|
|
17
|
+
},
|
|
18
|
+
"action": {
|
|
19
|
+
"default_popup": "popup.html",
|
|
20
|
+
"default_title": "browser-link",
|
|
21
|
+
"default_icon": {
|
|
22
|
+
"16": "icons/icon-16.png",
|
|
23
|
+
"32": "icons/icon-32.png",
|
|
24
|
+
"48": "icons/icon-48.png",
|
|
25
|
+
"128": "icons/icon-128.png"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -1,88 +1,88 @@
|
|
|
1
|
-
<!DOCTYPE html>
|
|
2
|
-
<html lang="es">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="UTF-8" />
|
|
5
|
-
<title>browser-link</title>
|
|
6
|
-
<style>
|
|
7
|
-
:root {
|
|
8
|
-
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
9
|
-
}
|
|
10
|
-
body {
|
|
11
|
-
margin: 0;
|
|
12
|
-
padding: 14px;
|
|
13
|
-
width: 280px;
|
|
14
|
-
color: #1f2937;
|
|
15
|
-
}
|
|
16
|
-
h1 {
|
|
17
|
-
font-size: 14px;
|
|
18
|
-
font-weight: 600;
|
|
19
|
-
margin: 0 0 10px;
|
|
20
|
-
letter-spacing: 0.3px;
|
|
21
|
-
}
|
|
22
|
-
.status {
|
|
23
|
-
padding: 8px 10px;
|
|
24
|
-
border-radius: 6px;
|
|
25
|
-
margin-bottom: 8px;
|
|
26
|
-
font-size: 12px;
|
|
27
|
-
line-height: 1.4;
|
|
28
|
-
}
|
|
29
|
-
.status.connected {
|
|
30
|
-
background: #d1fae5;
|
|
31
|
-
color: #065f46;
|
|
32
|
-
}
|
|
33
|
-
.status.disconnected {
|
|
34
|
-
background: #fef3c7;
|
|
35
|
-
color: #92400e;
|
|
36
|
-
}
|
|
37
|
-
.status.error {
|
|
38
|
-
background: #fee2e2;
|
|
39
|
-
color: #991b1b;
|
|
40
|
-
}
|
|
41
|
-
.url {
|
|
42
|
-
font-size: 11px;
|
|
43
|
-
color: #6b7280;
|
|
44
|
-
word-break: break-all;
|
|
45
|
-
margin-bottom: 10px;
|
|
46
|
-
max-height: 36px;
|
|
47
|
-
overflow: hidden;
|
|
48
|
-
}
|
|
49
|
-
.tab-id {
|
|
50
|
-
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
51
|
-
font-weight: 600;
|
|
52
|
-
}
|
|
53
|
-
button {
|
|
54
|
-
width: 100%;
|
|
55
|
-
padding: 9px 12px;
|
|
56
|
-
border: none;
|
|
57
|
-
border-radius: 6px;
|
|
58
|
-
cursor: pointer;
|
|
59
|
-
font-size: 13px;
|
|
60
|
-
font-weight: 500;
|
|
61
|
-
transition: opacity 0.15s ease;
|
|
62
|
-
}
|
|
63
|
-
button.primary {
|
|
64
|
-
background: #2563eb;
|
|
65
|
-
color: white;
|
|
66
|
-
}
|
|
67
|
-
button.danger {
|
|
68
|
-
background: #dc2626;
|
|
69
|
-
color: white;
|
|
70
|
-
}
|
|
71
|
-
button:hover {
|
|
72
|
-
opacity: 0.9;
|
|
73
|
-
}
|
|
74
|
-
button:disabled {
|
|
75
|
-
background: #9ca3af;
|
|
76
|
-
cursor: not-allowed;
|
|
77
|
-
opacity: 0.6;
|
|
78
|
-
}
|
|
79
|
-
</style>
|
|
80
|
-
</head>
|
|
81
|
-
<body>
|
|
82
|
-
<h1>browser-link</h1>
|
|
83
|
-
<div id="status" class="status disconnected">Cargando…</div>
|
|
84
|
-
<div id="url" class="url"></div>
|
|
85
|
-
<button id="action" class="primary" disabled>…</button>
|
|
86
|
-
<script type="module" src="popup.js"></script>
|
|
87
|
-
</body>
|
|
88
|
-
</html>
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="es">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<title>browser-link</title>
|
|
6
|
+
<style>
|
|
7
|
+
:root {
|
|
8
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
9
|
+
}
|
|
10
|
+
body {
|
|
11
|
+
margin: 0;
|
|
12
|
+
padding: 14px;
|
|
13
|
+
width: 280px;
|
|
14
|
+
color: #1f2937;
|
|
15
|
+
}
|
|
16
|
+
h1 {
|
|
17
|
+
font-size: 14px;
|
|
18
|
+
font-weight: 600;
|
|
19
|
+
margin: 0 0 10px;
|
|
20
|
+
letter-spacing: 0.3px;
|
|
21
|
+
}
|
|
22
|
+
.status {
|
|
23
|
+
padding: 8px 10px;
|
|
24
|
+
border-radius: 6px;
|
|
25
|
+
margin-bottom: 8px;
|
|
26
|
+
font-size: 12px;
|
|
27
|
+
line-height: 1.4;
|
|
28
|
+
}
|
|
29
|
+
.status.connected {
|
|
30
|
+
background: #d1fae5;
|
|
31
|
+
color: #065f46;
|
|
32
|
+
}
|
|
33
|
+
.status.disconnected {
|
|
34
|
+
background: #fef3c7;
|
|
35
|
+
color: #92400e;
|
|
36
|
+
}
|
|
37
|
+
.status.error {
|
|
38
|
+
background: #fee2e2;
|
|
39
|
+
color: #991b1b;
|
|
40
|
+
}
|
|
41
|
+
.url {
|
|
42
|
+
font-size: 11px;
|
|
43
|
+
color: #6b7280;
|
|
44
|
+
word-break: break-all;
|
|
45
|
+
margin-bottom: 10px;
|
|
46
|
+
max-height: 36px;
|
|
47
|
+
overflow: hidden;
|
|
48
|
+
}
|
|
49
|
+
.tab-id {
|
|
50
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
51
|
+
font-weight: 600;
|
|
52
|
+
}
|
|
53
|
+
button {
|
|
54
|
+
width: 100%;
|
|
55
|
+
padding: 9px 12px;
|
|
56
|
+
border: none;
|
|
57
|
+
border-radius: 6px;
|
|
58
|
+
cursor: pointer;
|
|
59
|
+
font-size: 13px;
|
|
60
|
+
font-weight: 500;
|
|
61
|
+
transition: opacity 0.15s ease;
|
|
62
|
+
}
|
|
63
|
+
button.primary {
|
|
64
|
+
background: #2563eb;
|
|
65
|
+
color: white;
|
|
66
|
+
}
|
|
67
|
+
button.danger {
|
|
68
|
+
background: #dc2626;
|
|
69
|
+
color: white;
|
|
70
|
+
}
|
|
71
|
+
button:hover {
|
|
72
|
+
opacity: 0.9;
|
|
73
|
+
}
|
|
74
|
+
button:disabled {
|
|
75
|
+
background: #9ca3af;
|
|
76
|
+
cursor: not-allowed;
|
|
77
|
+
opacity: 0.6;
|
|
78
|
+
}
|
|
79
|
+
</style>
|
|
80
|
+
</head>
|
|
81
|
+
<body>
|
|
82
|
+
<h1>browser-link</h1>
|
|
83
|
+
<div id="status" class="status disconnected">Cargando…</div>
|
|
84
|
+
<div id="url" class="url"></div>
|
|
85
|
+
<button id="action" class="primary" disabled>…</button>
|
|
86
|
+
<script type="module" src="popup.js"></script>
|
|
87
|
+
</body>
|
|
88
|
+
</html>
|
package/dist/map/db.js
CHANGED
|
@@ -42,34 +42,34 @@ function migrateLegacyDb(targetPath) {
|
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
function runMigrations(db) {
|
|
45
|
-
db.exec(`
|
|
46
|
-
CREATE TABLE IF NOT EXISTS apps (
|
|
47
|
-
id INTEGER PRIMARY KEY,
|
|
48
|
-
origin TEXT NOT NULL,
|
|
49
|
-
app_key TEXT NOT NULL,
|
|
50
|
-
title TEXT,
|
|
51
|
-
notes TEXT,
|
|
52
|
-
created_at TEXT NOT NULL,
|
|
53
|
-
last_seen_at TEXT NOT NULL,
|
|
54
|
-
UNIQUE(origin, app_key)
|
|
55
|
-
);
|
|
56
|
-
|
|
57
|
-
CREATE TABLE IF NOT EXISTS entries (
|
|
58
|
-
id INTEGER PRIMARY KEY,
|
|
59
|
-
app_id INTEGER NOT NULL REFERENCES apps(id) ON DELETE CASCADE,
|
|
60
|
-
url_pattern TEXT NOT NULL,
|
|
61
|
-
kind TEXT NOT NULL CHECK (kind IN ('selector', 'flow', 'gotcha')),
|
|
62
|
-
purpose TEXT NOT NULL,
|
|
63
|
-
payload TEXT NOT NULL,
|
|
64
|
-
verified_at TEXT,
|
|
65
|
-
failed_at TEXT,
|
|
66
|
-
notes TEXT,
|
|
67
|
-
created_at TEXT NOT NULL,
|
|
68
|
-
updated_at TEXT NOT NULL,
|
|
69
|
-
UNIQUE(app_id, url_pattern, kind, purpose)
|
|
70
|
-
);
|
|
71
|
-
|
|
72
|
-
CREATE INDEX IF NOT EXISTS idx_entries_lookup ON entries(app_id, url_pattern);
|
|
45
|
+
db.exec(`
|
|
46
|
+
CREATE TABLE IF NOT EXISTS apps (
|
|
47
|
+
id INTEGER PRIMARY KEY,
|
|
48
|
+
origin TEXT NOT NULL,
|
|
49
|
+
app_key TEXT NOT NULL,
|
|
50
|
+
title TEXT,
|
|
51
|
+
notes TEXT,
|
|
52
|
+
created_at TEXT NOT NULL,
|
|
53
|
+
last_seen_at TEXT NOT NULL,
|
|
54
|
+
UNIQUE(origin, app_key)
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
CREATE TABLE IF NOT EXISTS entries (
|
|
58
|
+
id INTEGER PRIMARY KEY,
|
|
59
|
+
app_id INTEGER NOT NULL REFERENCES apps(id) ON DELETE CASCADE,
|
|
60
|
+
url_pattern TEXT NOT NULL,
|
|
61
|
+
kind TEXT NOT NULL CHECK (kind IN ('selector', 'flow', 'gotcha')),
|
|
62
|
+
purpose TEXT NOT NULL,
|
|
63
|
+
payload TEXT NOT NULL,
|
|
64
|
+
verified_at TEXT,
|
|
65
|
+
failed_at TEXT,
|
|
66
|
+
notes TEXT,
|
|
67
|
+
created_at TEXT NOT NULL,
|
|
68
|
+
updated_at TEXT NOT NULL,
|
|
69
|
+
UNIQUE(app_id, url_pattern, kind, purpose)
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
CREATE INDEX IF NOT EXISTS idx_entries_lookup ON entries(app_id, url_pattern);
|
|
73
73
|
`);
|
|
74
74
|
}
|
|
75
75
|
export function closeDb() {
|
package/dist/map/queries.js
CHANGED
|
@@ -49,7 +49,7 @@ export function upsertApp(input) {
|
|
|
49
49
|
return db.prepare('SELECT * FROM apps WHERE id = ?').get(existing.id);
|
|
50
50
|
}
|
|
51
51
|
const info = db
|
|
52
|
-
.prepare(`INSERT INTO apps (origin, app_key, title, notes, created_at, last_seen_at)
|
|
52
|
+
.prepare(`INSERT INTO apps (origin, app_key, title, notes, created_at, last_seen_at)
|
|
53
53
|
VALUES (?, ?, ?, ?, ?, ?)`)
|
|
54
54
|
.run(input.origin, app_key, input.title ?? null, input.notes ?? null, ts, ts);
|
|
55
55
|
return db.prepare('SELECT * FROM apps WHERE id = ?').get(info.lastInsertRowid);
|
|
@@ -67,8 +67,8 @@ export function saveEntry(input) {
|
|
|
67
67
|
.prepare(`SELECT * FROM entries WHERE app_id = ? AND url_pattern = ? AND kind = ? AND purpose = ?`)
|
|
68
68
|
.get(app.id, input.url_pattern, input.kind, input.purpose);
|
|
69
69
|
if (existing) {
|
|
70
|
-
db.prepare(`UPDATE entries
|
|
71
|
-
SET payload = ?, notes = COALESCE(?, notes), verified_at = ?, failed_at = NULL, updated_at = ?
|
|
70
|
+
db.prepare(`UPDATE entries
|
|
71
|
+
SET payload = ?, notes = COALESCE(?, notes), verified_at = ?, failed_at = NULL, updated_at = ?
|
|
72
72
|
WHERE id = ?`).run(payloadJson, input.notes ?? null, ts, ts, existing.id);
|
|
73
73
|
const updated = db
|
|
74
74
|
.prepare('SELECT * FROM entries WHERE id = ?')
|
|
@@ -76,7 +76,7 @@ export function saveEntry(input) {
|
|
|
76
76
|
return { app, entry: hydrate(updated) };
|
|
77
77
|
}
|
|
78
78
|
const info = db
|
|
79
|
-
.prepare(`INSERT INTO entries (app_id, url_pattern, kind, purpose, payload, verified_at, notes, created_at, updated_at)
|
|
79
|
+
.prepare(`INSERT INTO entries (app_id, url_pattern, kind, purpose, payload, verified_at, notes, created_at, updated_at)
|
|
80
80
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
81
81
|
.run(app.id, input.url_pattern, input.kind, input.purpose, payloadJson, ts, input.notes ?? null, ts, ts);
|
|
82
82
|
const inserted = db
|