@jobshimo/browser-link 0.0.1 → 0.1.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/LICENSE +21 -21
- package/README.md +102 -83
- package/dist/cli.js +19 -19
- package/dist/commands/about.d.ts +17 -15
- package/dist/commands/about.js +161 -186
- package/dist/commands/about.js.map +1 -1
- package/dist/commands/menu.d.ts +2 -3
- package/dist/commands/menu.js +111 -131
- package/dist/commands/menu.js.map +1 -1
- package/dist/commands/welcome.d.ts +8 -7
- package/dist/commands/welcome.js +78 -121
- package/dist/commands/welcome.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/installers/index.d.ts +2 -3
- package/dist/installers/index.js +4 -4
- package/dist/installers/index.js.map +1 -1
- package/dist/installers/opencode.js +49 -21
- package/dist/installers/opencode.js.map +1 -1
- package/dist/map/db.js +28 -28
- package/dist/map/queries.js +4 -4
- package/dist/tools/server-instructions.js +46 -46
- package/package.json +64 -61
- package/dist/commands/tty.d.ts +0 -51
- package/dist/commands/tty.js +0 -148
- package/dist/commands/tty.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"welcome.js","sourceRoot":"","sources":["../../src/commands/welcome.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"welcome.js","sourceRoot":"","sources":["../../src/commands/welcome.ts"],"names":[],"mappings":"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"}
|
|
@@ -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.0
|
|
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
|
+
{
|
|
2
|
+
"manifest_version": 3,
|
|
3
|
+
"name": "browser-link",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"description": "Bridge between Chrome and an MCP client (Claude Code, OpenCode, …). 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>
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import type { ClientId, Installer } from './types.js';
|
|
2
2
|
/**
|
|
3
|
-
* Installers
|
|
4
|
-
*
|
|
5
|
-
* will never invoke its (throwing) install/uninstall stubs.
|
|
3
|
+
* Installers wired into the CLI surface (`install`, `install --client X`,
|
|
4
|
+
* the interactive menu, and `doctor`). Order here is the display order.
|
|
6
5
|
*/
|
|
7
6
|
export declare const INSTALLERS: Installer[];
|
|
8
7
|
export declare function getInstaller(id: ClientId): Installer;
|
package/dist/installers/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { claudeInstaller } from './claude.js';
|
|
2
|
+
import { opencodeInstaller } from './opencode.js';
|
|
2
3
|
/**
|
|
3
|
-
* Installers
|
|
4
|
-
*
|
|
5
|
-
* will never invoke its (throwing) install/uninstall stubs.
|
|
4
|
+
* Installers wired into the CLI surface (`install`, `install --client X`,
|
|
5
|
+
* the interactive menu, and `doctor`). Order here is the display order.
|
|
6
6
|
*/
|
|
7
|
-
export const INSTALLERS = [claudeInstaller];
|
|
7
|
+
export const INSTALLERS = [claudeInstaller, opencodeInstaller];
|
|
8
8
|
export function getInstaller(id) {
|
|
9
9
|
const found = INSTALLERS.find((i) => i.id === id);
|
|
10
10
|
if (!found)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/installers/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/installers/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAGlD;;;GAGG;AACH,MAAM,CAAC,MAAM,UAAU,GAAgB,CAAC,eAAe,EAAE,iBAAiB,CAAC,CAAC;AAE5E,MAAM,UAAU,YAAY,CAAC,EAAY;IACvC,MAAM,KAAK,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;IAClD,IAAI,CAAC,KAAK;QAAE,MAAM,IAAI,KAAK,CAAC,kCAAkC,EAAE,EAAE,CAAC,CAAC;IACpE,OAAO,KAAK,CAAC;AACf,CAAC"}
|
|
@@ -1,23 +1,29 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs';
|
|
2
|
-
import { homedir
|
|
3
|
-
import { join } from 'node:path';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
* detect() looks for the conventional config path per OS; install/uninstall
|
|
7
|
-
* throw with a clear message so the CLI surface is consistent.
|
|
8
|
-
*/
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
const SERVER_NAME = 'browser-link';
|
|
5
|
+
const SCHEMA_URL = 'https://opencode.ai/config.json';
|
|
9
6
|
function configFile() {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
7
|
+
// OpenCode uses ~/.config/opencode/opencode.json on every OS, Windows included
|
|
8
|
+
// (verified against an actual install — not %APPDATA% as it might seem).
|
|
9
|
+
return join(homedir(), '.config', 'opencode', 'opencode.json');
|
|
10
|
+
}
|
|
11
|
+
function readConfig(path) {
|
|
12
|
+
if (!existsSync(path))
|
|
13
|
+
return {};
|
|
14
|
+
try {
|
|
15
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
13
16
|
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
return join(xdg, 'opencode', 'opencode.json');
|
|
17
|
-
if (platform() === 'darwin') {
|
|
18
|
-
return join(homedir(), 'Library', 'Application Support', 'opencode', 'opencode.json');
|
|
17
|
+
catch {
|
|
18
|
+
throw new Error(`Could not parse OpenCode config at ${path}. Fix the file or delete it.`);
|
|
19
19
|
}
|
|
20
|
-
|
|
20
|
+
}
|
|
21
|
+
function writeConfig(path, cfg) {
|
|
22
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
23
|
+
writeFileSync(path, JSON.stringify(cfg, null, 2) + '\n', 'utf8');
|
|
24
|
+
}
|
|
25
|
+
function isRegistered(cfg) {
|
|
26
|
+
return !!cfg.mcp?.[SERVER_NAME];
|
|
21
27
|
}
|
|
22
28
|
export const opencodeInstaller = {
|
|
23
29
|
id: 'opencode',
|
|
@@ -27,13 +33,35 @@ export const opencodeInstaller = {
|
|
|
27
33
|
},
|
|
28
34
|
detect() {
|
|
29
35
|
const path = configFile();
|
|
30
|
-
|
|
36
|
+
if (!existsSync(path)) {
|
|
37
|
+
return { installed: false, registered: false, configPath: path };
|
|
38
|
+
}
|
|
39
|
+
const cfg = readConfig(path);
|
|
40
|
+
return { installed: true, registered: isRegistered(cfg), configPath: path };
|
|
31
41
|
},
|
|
32
|
-
install() {
|
|
33
|
-
|
|
42
|
+
install(command, args) {
|
|
43
|
+
const path = configFile();
|
|
44
|
+
const cfg = readConfig(path);
|
|
45
|
+
if (!cfg.$schema)
|
|
46
|
+
cfg.$schema = SCHEMA_URL;
|
|
47
|
+
cfg.mcp = cfg.mcp ?? {};
|
|
48
|
+
const existing = cfg.mcp[SERVER_NAME];
|
|
49
|
+
cfg.mcp[SERVER_NAME] = { type: 'local', command: [command, ...args] };
|
|
50
|
+
writeConfig(path, cfg);
|
|
51
|
+
return existing
|
|
52
|
+
? `Updated ${SERVER_NAME} entry in ${path}.`
|
|
53
|
+
: `Added ${SERVER_NAME} entry to ${path}.`;
|
|
34
54
|
},
|
|
35
55
|
uninstall() {
|
|
36
|
-
|
|
56
|
+
const path = configFile();
|
|
57
|
+
if (!existsSync(path))
|
|
58
|
+
return `No OpenCode config at ${path}; nothing to remove.`;
|
|
59
|
+
const cfg = readConfig(path);
|
|
60
|
+
if (!cfg.mcp?.[SERVER_NAME])
|
|
61
|
+
return `${SERVER_NAME} was not registered in ${path}.`;
|
|
62
|
+
delete cfg.mcp[SERVER_NAME];
|
|
63
|
+
writeConfig(path, cfg);
|
|
64
|
+
return `Removed ${SERVER_NAME} entry from ${path}.`;
|
|
37
65
|
},
|
|
38
66
|
};
|
|
39
67
|
//# sourceMappingURL=opencode.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"opencode.js","sourceRoot":"","sources":["../../src/installers/opencode.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"opencode.js","sourceRoot":"","sources":["../../src/installers/opencode.ts"],"names":[],"mappings":"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"}
|