gravity-lite 3.0.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.
@@ -0,0 +1,197 @@
1
+ // =============================================================================
2
+ // Gravity v3 — Service Worker (MV3)
3
+ //
4
+ // Responsibilities:
5
+ // 1. Manage chrome.debugger attachment (only works in SW context)
6
+ // 2. Ensure the offscreen document (WebSocket bridge) is always alive
7
+ // 3. Route messages between offscreen doc ↔ popup ↔ CDP
8
+ //
9
+ // The offscreen document owns the persistent WebSocket to the MCP server.
10
+ // The SW wakes on demand to execute CDP commands via chrome.debugger.
11
+ // =============================================================================
12
+
13
+ let debuggerState = {
14
+ attached: false,
15
+ tabId: null,
16
+ domainsEnabled: false,
17
+ lastError: null,
18
+ attachmentTime: null,
19
+ };
20
+
21
+ let wsConnected = false; // reflects offscreen WS status
22
+
23
+ // ── Offscreen document management ────────────────────────────────────────────
24
+
25
+ async function ensureOffscreen() {
26
+ // chrome.offscreen available since Chrome 116
27
+ const existing = await chrome.offscreen.hasDocument().catch(() => false);
28
+ if (!existing) {
29
+ await chrome.offscreen.createDocument({
30
+ url: 'offscreen.html',
31
+ reasons: ['WORKERS'],
32
+ justification: 'Maintain persistent WebSocket connection to local MCP server bridge',
33
+ }).catch(() => {
34
+ // May fail if already being created — ignore
35
+ });
36
+ }
37
+ }
38
+
39
+ // ── Message routing ───────────────────────────────────────────────────────────
40
+
41
+ chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => {
42
+
43
+ // ── From offscreen document ───────────────────────────────────────────────
44
+ if (msg.to === 'sw') {
45
+
46
+ if (msg.type === 'ws_status') {
47
+ wsConnected = msg.connected;
48
+ // Broadcast to popup if open
49
+ chrome.runtime.sendMessage({ action: 'ws_status_update', connected: msg.connected }).catch(() => {});
50
+ return;
51
+ }
52
+
53
+ if (msg.type === 'cdp_request') {
54
+ // Execute CDP via chrome.debugger, send response back to offscreen
55
+ const { id, method, params } = msg.payload;
56
+
57
+ if (!debuggerState.attached || !debuggerState.tabId) {
58
+ chrome.runtime.sendMessage({
59
+ to: 'offscreen',
60
+ type: 'cdp_response',
61
+ payload: { type: 'cdp_response', id, error: { message: 'Debugger not attached. Click "Connect to Tab" in the Gravity popup.' } }
62
+ }).catch(() => {});
63
+ return;
64
+ }
65
+
66
+ chrome.debugger.sendCommand({ tabId: debuggerState.tabId }, method, params || {}, (result) => {
67
+ const response = chrome.runtime.lastError
68
+ ? { type: 'cdp_response', id, error: { message: chrome.runtime.lastError.message } }
69
+ : { type: 'cdp_response', id, result };
70
+
71
+ chrome.runtime.sendMessage({ to: 'offscreen', type: 'cdp_response', payload: response }).catch(() => {});
72
+ });
73
+ return;
74
+ }
75
+ }
76
+
77
+ // ── From popup ────────────────────────────────────────────────────────────
78
+
79
+ if (msg.action === 'attach') {
80
+ chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
81
+ if (!tabs?.[0]?.id) return sendResponse({ success: false, error: 'No active tab found' });
82
+ attachDebugger(tabs[0].id, sendResponse);
83
+ });
84
+ return true; // async
85
+ }
86
+
87
+ if (msg.action === 'detach') {
88
+ detachDebugger(sendResponse);
89
+ return true;
90
+ }
91
+
92
+ if (msg.action === 'status') {
93
+ sendResponse({ ...debuggerState, wsConnected });
94
+ return true;
95
+ }
96
+ });
97
+
98
+ // ── Debugger management ───────────────────────────────────────────────────────
99
+
100
+ function attachDebugger(tabId, callback) {
101
+ debuggerState.lastError = null;
102
+
103
+ chrome.tabs.get(tabId, (tab) => {
104
+ if (chrome.runtime.lastError || !tab) {
105
+ const err = chrome.runtime.lastError?.message || 'Tab not found';
106
+ debuggerState.lastError = err;
107
+ return callback({ success: false, error: err });
108
+ }
109
+
110
+ if (tab.url?.startsWith('chrome://') || tab.url?.startsWith('chrome-extension://')) {
111
+ const err = `Cannot attach to browser page: ${tab.url}`;
112
+ debuggerState.lastError = err;
113
+ return callback({ success: false, error: err });
114
+ }
115
+
116
+ // Detach from previous tab if different
117
+ if (debuggerState.attached && debuggerState.tabId && debuggerState.tabId !== tabId) {
118
+ chrome.debugger.detach({ tabId: debuggerState.tabId }, () => {});
119
+ }
120
+
121
+ chrome.debugger.attach({ tabId }, '1.3', () => {
122
+ if (chrome.runtime.lastError) {
123
+ const err = chrome.runtime.lastError.message;
124
+ debuggerState.lastError = err;
125
+ debuggerState.attached = false;
126
+ return callback({ success: false, error: err });
127
+ }
128
+
129
+ debuggerState.attached = true;
130
+ debuggerState.tabId = tabId;
131
+ debuggerState.domainsEnabled = false;
132
+ debuggerState.attachmentTime = Date.now();
133
+
134
+ enableCDPDomains(tabId, () => {
135
+ debuggerState.domainsEnabled = true;
136
+ // Make sure offscreen WS bridge is running
137
+ ensureOffscreen();
138
+ callback({ success: true, tabId });
139
+ });
140
+ });
141
+ });
142
+ }
143
+
144
+ function detachDebugger(callback) {
145
+ if (!debuggerState.tabId) {
146
+ debuggerState = { attached: false, tabId: null, domainsEnabled: false, lastError: null, attachmentTime: null };
147
+ return callback({ success: true });
148
+ }
149
+ chrome.debugger.detach({ tabId: debuggerState.tabId }, () => {
150
+ debuggerState = { attached: false, tabId: null, domainsEnabled: false, lastError: null, attachmentTime: null };
151
+ callback({ success: true });
152
+ });
153
+ }
154
+
155
+ function enableCDPDomains(tabId, done) {
156
+ const domains = ['DOM', 'CSS', 'Page', 'Overlay'];
157
+ let i = 0;
158
+ const next = () => {
159
+ if (i >= domains.length) return done();
160
+ const domain = domains[i++];
161
+ chrome.debugger.sendCommand({ tabId }, `${domain}.enable`, {}, () => {
162
+ if (chrome.runtime.lastError) {
163
+ console.warn(`[gravity] failed to enable ${domain}:`, chrome.runtime.lastError.message);
164
+ }
165
+ next();
166
+ });
167
+ };
168
+ next();
169
+ }
170
+
171
+ // ── Chrome events ─────────────────────────────────────────────────────────────
172
+
173
+ chrome.debugger.onDetach.addListener((source, reason) => {
174
+ if (source.tabId === debuggerState.tabId) {
175
+ debuggerState = {
176
+ attached: false, tabId: null, domainsEnabled: false,
177
+ lastError: reason === 'target_closed' ? 'Tab was closed' : `Detached: ${reason}`,
178
+ attachmentTime: null,
179
+ };
180
+ }
181
+ chrome.runtime.sendMessage({ action: 'debugger_detached', tabId: source.tabId, reason }).catch(() => {});
182
+ });
183
+
184
+ chrome.tabs.onUpdated.addListener((tabId, changeInfo) => {
185
+ if (tabId === debuggerState.tabId && changeInfo.status === 'loading') {
186
+ debuggerState.domainsEnabled = false;
187
+ }
188
+ });
189
+
190
+ chrome.tabs.onRemoved.addListener((tabId) => {
191
+ if (tabId === debuggerState.tabId) {
192
+ debuggerState = { attached: false, tabId: null, domainsEnabled: false, lastError: 'Tab was closed', attachmentTime: null };
193
+ }
194
+ });
195
+
196
+ // ── Boot — ensure offscreen doc is alive on SW startup ───────────────────────
197
+ ensureOffscreen();
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor" class="icon icon-tabler icons-tabler-filled icon-tabler-settings"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M14.647 4.081a.724 .724 0 0 0 1.08 .448c2.439 -1.485 5.23 1.305 3.745 3.744a.724 .724 0 0 0 .447 1.08c2.775 .673 2.775 4.62 0 5.294a.724 .724 0 0 0 -.448 1.08c1.485 2.439 -1.305 5.23 -3.744 3.745a.724 .724 0 0 0 -1.08 .447c-.673 2.775 -4.62 2.775 -5.294 0a.724 .724 0 0 0 -1.08 -.448c-2.439 1.485 -5.23 -1.305 -3.745 -3.744a.724 .724 0 0 0 -.447 -1.08c-2.775 -.673 -2.775 -4.62 0 -5.294a.724 .724 0 0 0 .448 -1.08c-1.485 -2.439 1.305 -5.23 3.744 -3.745a.722 .722 0 0 0 1.08 -.447c.673 -2.775 4.62 -2.775 5.294 0zm-2.647 4.919a3 3 0 1 0 0 6a3 3 0 0 0 0 -6z" /></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-rocket"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M4 13a8 8 0 0 1 7 7a6 6 0 0 0 3 -5a9 9 0 0 0 6 -8a3 3 0 0 0 -3 -3a9 9 0 0 0 -8 6a6 6 0 0 0 -5 3" /><path d="M7 14a6 6 0 0 0 -3 6a6 6 0 0 0 6 -3" /><path d="M15 9m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" /></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-world"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M3 12a9 9 0 1 0 18 0a9 9 0 0 0 -18 0" /><path d="M3.6 9h16.8" /><path d="M3.6 15h16.8" /><path d="M11.5 3a17 17 0 0 0 0 18" /><path d="M12.5 3a17 17 0 0 1 0 18" /></svg>
@@ -0,0 +1,24 @@
1
+ {
2
+ "manifest_version": 3,
3
+ "name": "Gravity",
4
+ "version": "3.0.0",
5
+ "description": "AI-powered CSS layout diagnostics — connects to local MCP server",
6
+ "permissions": [
7
+ "debugger",
8
+ "activeTab",
9
+ "tabs",
10
+ "offscreen"
11
+ ],
12
+ "background": {
13
+ "service_worker": "background.js",
14
+ "type": "module"
15
+ },
16
+ "action": {
17
+ "default_popup": "popup.html",
18
+ "default_icon": {
19
+ "16": "icon16.svg",
20
+ "48": "icon48.svg",
21
+ "128": "icon128.svg"
22
+ }
23
+ }
24
+ }
@@ -0,0 +1,4 @@
1
+ <!DOCTYPE html>
2
+ <!-- Gravity offscreen document — holds the WebSocket bridge alive permanently.
3
+ Chrome MV3 offscreen documents with an active WebSocket are not suspended. -->
4
+ <script src="offscreen.js"></script>
@@ -0,0 +1,74 @@
1
+ // =============================================================================
2
+ // Gravity v3 — Offscreen Document
3
+ //
4
+ // Lives permanently (Chrome keeps offscreen docs alive while a WebSocket is
5
+ // open). Owns the WS connection to the MCP server bridge on :9224.
6
+ //
7
+ // Message flow:
8
+ // SW → chrome.runtime.sendMessage({ to:'offscreen', ... }) → here
9
+ // here → chrome.runtime.sendMessage({ to:'sw', ... }) → SW
10
+ // =============================================================================
11
+
12
+ const WS_PORT = 9224;
13
+ const RECONNECT_MS = 2000;
14
+
15
+ let ws = null;
16
+ let wsReady = false;
17
+
18
+ // ── WebSocket to MCP server ───────────────────────────────────────────────────
19
+
20
+ function connect() {
21
+ if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) return;
22
+
23
+ try {
24
+ ws = new WebSocket(`ws://127.0.0.1:${WS_PORT}`);
25
+
26
+ ws.onopen = () => {
27
+ wsReady = true;
28
+ // Tell SW the bridge is up so it can update the popup
29
+ chrome.runtime.sendMessage({ to: 'sw', type: 'ws_status', connected: true }).catch(() => {});
30
+ };
31
+
32
+ ws.onmessage = ({ data }) => {
33
+ try {
34
+ const msg = JSON.parse(data);
35
+ // Forward CDP requests from MCP server → SW (which has chrome.debugger)
36
+ if (msg.type === 'cdp_request') {
37
+ chrome.runtime.sendMessage({ to: 'sw', type: 'cdp_request', payload: msg }).catch(() => {});
38
+ }
39
+ } catch (e) {
40
+ console.error('[gravity/offscreen] parse error', e);
41
+ }
42
+ };
43
+
44
+ ws.onclose = () => {
45
+ wsReady = false;
46
+ ws = null;
47
+ chrome.runtime.sendMessage({ to: 'sw', type: 'ws_status', connected: false }).catch(() => {});
48
+ setTimeout(connect, RECONNECT_MS);
49
+ };
50
+
51
+ ws.onerror = () => {
52
+ // onclose fires right after, handles retry
53
+ };
54
+ } catch (e) {
55
+ console.error('[gravity/offscreen] WS create failed', e);
56
+ setTimeout(connect, RECONNECT_MS);
57
+ }
58
+ }
59
+
60
+ // ── Receive CDP responses from SW, forward to MCP server ─────────────────────
61
+
62
+ chrome.runtime.onMessage.addListener((msg) => {
63
+ if (msg.to !== 'offscreen') return;
64
+
65
+ if (msg.type === 'cdp_response') {
66
+ if (ws && ws.readyState === WebSocket.OPEN) {
67
+ ws.send(JSON.stringify(msg.payload));
68
+ }
69
+ }
70
+ });
71
+
72
+ // ── Boot ──────────────────────────────────────────────────────────────────────
73
+
74
+ connect();
@@ -0,0 +1,105 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <style>
6
+ * { box-sizing: border-box; margin: 0; padding: 0; }
7
+ body {
8
+ width: 280px;
9
+ padding: 16px;
10
+ font-family: system-ui, -apple-system, sans-serif;
11
+ font-size: 13px;
12
+ color: #1a1a1a;
13
+ background: #fff;
14
+ }
15
+ h3 { font-size: 15px; font-weight: 600; margin-bottom: 12px; }
16
+
17
+ .row {
18
+ display: flex;
19
+ align-items: center;
20
+ gap: 8px;
21
+ margin-bottom: 8px;
22
+ }
23
+ .dot {
24
+ width: 10px; height: 10px;
25
+ border-radius: 50%;
26
+ flex-shrink: 0;
27
+ }
28
+ .dot.green { background: #22c55e; }
29
+ .dot.red { background: #ef4444; }
30
+ .dot.yellow { background: #eab308; }
31
+
32
+ .label { color: #555; min-width: 80px; }
33
+ .value { font-weight: 500; }
34
+
35
+ hr { border: none; border-top: 1px solid #eee; margin: 12px 0; }
36
+
37
+ button {
38
+ width: 100%;
39
+ padding: 8px 12px;
40
+ border: none;
41
+ border-radius: 6px;
42
+ font-size: 13px;
43
+ font-weight: 500;
44
+ cursor: pointer;
45
+ transition: background 0.15s;
46
+ }
47
+ #toggleBtn {
48
+ background: #2563eb;
49
+ color: #fff;
50
+ margin-bottom: 6px;
51
+ }
52
+ #toggleBtn:hover { background: #1d4ed8; }
53
+ #toggleBtn.disconnect { background: #dc2626; }
54
+ #toggleBtn.disconnect:hover { background: #b91c1c; }
55
+
56
+ .hint {
57
+ font-size: 11px;
58
+ color: #888;
59
+ margin-top: 10px;
60
+ line-height: 1.5;
61
+ }
62
+ .hint code {
63
+ background: #f3f4f6;
64
+ padding: 1px 4px;
65
+ border-radius: 3px;
66
+ font-size: 11px;
67
+ }
68
+ .error-msg {
69
+ font-size: 11px;
70
+ color: #dc2626;
71
+ margin-top: 4px;
72
+ display: none;
73
+ }
74
+ </style>
75
+ </head>
76
+ <body>
77
+ <h3>⚡ Gravity</h3>
78
+
79
+ <div class="row">
80
+ <span class="label">Debugger</span>
81
+ <div id="debugDot" class="dot red"></div>
82
+ <span id="debugText" class="value">Disconnected</span>
83
+ </div>
84
+ <div class="row">
85
+ <span class="label">MCP Server</span>
86
+ <div id="mcpDot" class="dot red"></div>
87
+ <span id="mcpText" class="value">Not running</span>
88
+ </div>
89
+
90
+ <div id="errorMsg" class="error-msg"></div>
91
+
92
+ <hr>
93
+
94
+ <button id="toggleBtn">Connect to Tab</button>
95
+
96
+ <div class="hint">
97
+ <strong>Setup (once):</strong><br>
98
+ 1. Add <code>gravity</code> to your MCP config<br>
99
+ 2. Click "Connect to Tab" above<br>
100
+ 3. Ask AI: <em>"diagnose #header"</em>
101
+ </div>
102
+
103
+ <script src="popup.js"></script>
104
+ </body>
105
+ </html>
@@ -0,0 +1,84 @@
1
+ // popup.js — Gravity v3 (MV3 compatible)
2
+ const debugDot = document.getElementById('debugDot');
3
+ const debugText = document.getElementById('debugText');
4
+ const mcpDot = document.getElementById('mcpDot');
5
+ const mcpText = document.getElementById('mcpText');
6
+ const toggleBtn = document.getElementById('toggleBtn');
7
+ const errorMsg = document.getElementById('errorMsg');
8
+
9
+ function setDot(dot, text, state, label) {
10
+ dot.className = 'dot ' + state;
11
+ text.textContent = label;
12
+ }
13
+
14
+ function showError(msg) {
15
+ errorMsg.style.display = msg ? 'block' : 'none';
16
+ errorMsg.textContent = msg || '';
17
+ }
18
+
19
+ function render(status) {
20
+ showError(null);
21
+
22
+ // Debugger row
23
+ if (status.attached && status.domainsEnabled) {
24
+ setDot(debugDot, debugText, 'green', `Tab ${status.tabId}`);
25
+ } else if (status.attached) {
26
+ setDot(debugDot, debugText, 'yellow', 'Attaching…');
27
+ } else {
28
+ setDot(debugDot, debugText, 'red', 'Disconnected');
29
+ if (status.lastError) showError(status.lastError);
30
+ }
31
+
32
+ // MCP server row
33
+ if (status.wsConnected) {
34
+ setDot(mcpDot, mcpText, 'green', 'Connected');
35
+ } else {
36
+ setDot(mcpDot, mcpText, 'red', 'Not running');
37
+ }
38
+
39
+ // Button
40
+ if (status.attached) {
41
+ toggleBtn.textContent = 'Disconnect';
42
+ toggleBtn.classList.add('disconnect');
43
+ } else {
44
+ toggleBtn.textContent = 'Connect to Tab';
45
+ toggleBtn.classList.remove('disconnect');
46
+ }
47
+ }
48
+
49
+ function refresh() {
50
+ // In MV3, sendMessage may fail if SW is sleeping — retry once
51
+ chrome.runtime.sendMessage({ action: 'status' }, (status) => {
52
+ if (chrome.runtime.lastError) {
53
+ // SW waking up — retry after short delay
54
+ setTimeout(() => {
55
+ chrome.runtime.sendMessage({ action: 'status' }, (s) => {
56
+ if (!chrome.runtime.lastError && s) render(s);
57
+ });
58
+ }, 300);
59
+ return;
60
+ }
61
+ if (status) render(status);
62
+ });
63
+ }
64
+
65
+ toggleBtn.addEventListener('click', () => {
66
+ toggleBtn.disabled = true;
67
+ chrome.runtime.sendMessage({ action: 'status' }, (status) => {
68
+ const action = (status && status.attached) ? 'detach' : 'attach';
69
+ chrome.runtime.sendMessage({ action }, (response) => {
70
+ toggleBtn.disabled = false;
71
+ if (response && !response.success && response.error) showError(response.error);
72
+ refresh();
73
+ });
74
+ });
75
+ });
76
+
77
+ // Listen for real-time status pushes from SW (ws_status_update)
78
+ chrome.runtime.onMessage.addListener((msg) => {
79
+ if (msg.action === 'ws_status_update') refresh();
80
+ if (msg.action === 'debugger_detached') refresh();
81
+ });
82
+
83
+ refresh();
84
+ setInterval(refresh, 2000);
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "gravity-lite",
3
+ "version": "3.0.0",
4
+ "description": "AI-powered CSS layout diagnostics — zero-config MCP server for developers",
5
+ "type": "module",
6
+ "bin": {
7
+ "gravity": "./dist/cli.js"
8
+ },
9
+ "scripts": {
10
+ "build": "tsc",
11
+ "dev": "tsc --watch",
12
+ "start": "node dist/cli.js",
13
+ "prepublishOnly": "npm run build"
14
+ },
15
+ "engines": {
16
+ "node": ">=16"
17
+ },
18
+ "dependencies": {
19
+ "@modelcontextprotocol/sdk": "^1.0.0",
20
+ "commander": "^11.0.0",
21
+ "ws": "^8.14.0"
22
+ },
23
+ "devDependencies": {
24
+ "@types/node": "^20.0.0",
25
+ "@types/ws": "^8.5.0",
26
+ "typescript": "^5.0.0"
27
+ },
28
+ "files": [
29
+ "dist",
30
+ "extension",
31
+ "README.md",
32
+ "LICENSE"
33
+ ],
34
+ "keywords": [
35
+ "mcp",
36
+ "mcp-server",
37
+ "css",
38
+ "layout",
39
+ "diagnostics",
40
+ "devtools",
41
+ "chrome",
42
+ "ai",
43
+ "kiro",
44
+ "cursor",
45
+ "claude",
46
+ "accessibility",
47
+ "wcag"
48
+ ],
49
+ "license": "MIT",
50
+ "repository": {
51
+ "type": "git",
52
+ "url": "https://github.com/DharuNamikaze/gravity"
53
+ },
54
+ "homepage": "https://github.com/DharuNamikaze/gravity#readme",
55
+ "bugs": {
56
+ "url": "https://github.com/DharuNamikaze/gravity/issues"
57
+ },
58
+ "author": "DharuNamikaze"
59
+ }