pixelpick 2.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.
- package/LICENSE +21 -0
- package/README.md +233 -0
- package/dist/chrome-mv3/assets/sidepanel-D7qwGDau.css +1 -0
- package/dist/chrome-mv3/background.js +1 -0
- package/dist/chrome-mv3/chunks/sidepanel-CzCbYvqW.js +13 -0
- package/dist/chrome-mv3/content-scripts/content.js +34 -0
- package/dist/chrome-mv3/icon/128.png +0 -0
- package/dist/chrome-mv3/icon/16.png +0 -0
- package/dist/chrome-mv3/icon/48.png +0 -0
- package/dist/chrome-mv3/manifest.json +1 -0
- package/dist/chrome-mv3/sidepanel.html +13 -0
- package/dist/chrome-mv3/src/constants.js +63 -0
- package/dist/chrome-mv3/src/inspector/dom-inspector.js +165 -0
- package/dist/chrome-mv3/src/inspector/highlight.js +155 -0
- package/dist/chrome-mv3/src/inspector/index.js +82 -0
- package/dist/chrome-mv3/src/inspector/picker.js +101 -0
- package/dist/chrome-mv3/src/inspector/react-detector.js +225 -0
- package/package.json +75 -0
- package/public/icon/128.png +0 -0
- package/public/icon/16.png +0 -0
- package/public/icon/48.png +0 -0
- package/src/cli/index.js +278 -0
- package/src/hooks/check-selection.mjs +123 -0
- package/src/server/index.js +571 -0
- package/src/shared/constants.js +97 -0
|
@@ -0,0 +1,571 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import {
|
|
5
|
+
CallToolRequestSchema,
|
|
6
|
+
ListResourcesRequestSchema,
|
|
7
|
+
ListToolsRequestSchema,
|
|
8
|
+
ReadResourceRequestSchema,
|
|
9
|
+
} from '@modelcontextprotocol/sdk/types.js';
|
|
10
|
+
import { WebSocketServer } from 'ws';
|
|
11
|
+
import http from 'http';
|
|
12
|
+
import { execSync } from 'child_process';
|
|
13
|
+
import { PORTS, SELECTION } from '../shared/constants.js';
|
|
14
|
+
|
|
15
|
+
const WEBSOCKET_PORT = PORTS.WEBSOCKET;
|
|
16
|
+
const IPC_PORT = PORTS.IPC;
|
|
17
|
+
|
|
18
|
+
// Helper: Find process using a port
|
|
19
|
+
function findProcessOnPort(port) {
|
|
20
|
+
try {
|
|
21
|
+
const output = execSync(`lsof -i :${port} -t`, { encoding: 'utf8' }).trim();
|
|
22
|
+
const pid = parseInt(output.split('\n')[0]); // Get first PID
|
|
23
|
+
return pid || null;
|
|
24
|
+
} catch (err) {
|
|
25
|
+
return null; // No process found or lsof not available
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Helper: Handle port in use error
|
|
30
|
+
function handlePortInUse(port, portName = 'Port') {
|
|
31
|
+
const pid = findProcessOnPort(port);
|
|
32
|
+
|
|
33
|
+
if (pid) {
|
|
34
|
+
console.error(`\n[PixelPick] Error: ${portName} ${port} is already in use by process ${pid}`);
|
|
35
|
+
console.error(`\nTo fix this, kill the existing process:\n`);
|
|
36
|
+
console.error(` kill ${pid}`);
|
|
37
|
+
console.error(`\nThen restart PixelPick.\n`);
|
|
38
|
+
} else {
|
|
39
|
+
console.error(`\n[PixelPick] Error: ${portName} ${port} is already in use`);
|
|
40
|
+
console.error(`Try setting a different port using environment variables.\n`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// SelectionBuffer - stores element selections from Chrome extension (multi-selection mode)
|
|
47
|
+
class SelectionBuffer {
|
|
48
|
+
constructor(maxActive = SELECTION.MAX_ACTIVE, maxHistory = SELECTION.MAX_HISTORY) {
|
|
49
|
+
this.maxActive = maxActive;
|
|
50
|
+
this.maxHistory = maxHistory;
|
|
51
|
+
this.active = []; // Current working selections (max 10)
|
|
52
|
+
this.history = []; // Full history (max 50)
|
|
53
|
+
this.listeners = [];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Generate unique ID for selection
|
|
57
|
+
_generateId() {
|
|
58
|
+
const timestamp = Date.now();
|
|
59
|
+
const random = Math.random().toString(36).slice(2, 8);
|
|
60
|
+
return `sel_${timestamp}_${random}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
add(selection) {
|
|
64
|
+
const enrichedSelection = {
|
|
65
|
+
...selection,
|
|
66
|
+
id: this._generateId(),
|
|
67
|
+
timestamp: Date.now(),
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// Add to active with FIFO eviction
|
|
71
|
+
this.active.push(enrichedSelection);
|
|
72
|
+
if (this.active.length > this.maxActive) {
|
|
73
|
+
this.active.shift(); // Remove oldest
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Add to history separately
|
|
77
|
+
this.history.unshift(enrichedSelection);
|
|
78
|
+
if (this.history.length > this.maxHistory) {
|
|
79
|
+
this.history = this.history.slice(0, this.maxHistory);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Notify listeners
|
|
83
|
+
this.listeners.forEach(fn => fn(enrichedSelection));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
getActive() {
|
|
87
|
+
return this.active;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
removeActive(id) {
|
|
91
|
+
const index = this.active.findIndex(sel => sel.id === id);
|
|
92
|
+
if (index !== -1) {
|
|
93
|
+
const removed = this.active.splice(index, 1)[0];
|
|
94
|
+
return removed;
|
|
95
|
+
}
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
clearActive() {
|
|
100
|
+
this.active = [];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
getHistory(limit = 50) {
|
|
104
|
+
return this.history.slice(0, limit);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
clear() {
|
|
108
|
+
this.active = [];
|
|
109
|
+
this.history = [];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
onSelection(fn) {
|
|
113
|
+
this.listeners.push(fn);
|
|
114
|
+
return () => {
|
|
115
|
+
this.listeners = this.listeners.filter(l => l !== fn);
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const selectionBuffer = new SelectionBuffer();
|
|
121
|
+
|
|
122
|
+
// WebSocket Server - receives selections from Chrome extension
|
|
123
|
+
const wss = new WebSocketServer({ port: WEBSOCKET_PORT });
|
|
124
|
+
const activeConnections = new Set();
|
|
125
|
+
|
|
126
|
+
wss.on('listening', () => {
|
|
127
|
+
console.error(`[PixelPick] WebSocket server listening on port ${WEBSOCKET_PORT}`);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
wss.on('error', (error) => {
|
|
131
|
+
if (error.code === 'EADDRINUSE') {
|
|
132
|
+
handlePortInUse(WEBSOCKET_PORT, 'WebSocket port');
|
|
133
|
+
} else {
|
|
134
|
+
console.error('[PixelPick] WebSocket server error:', error.message);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
wss.on('connection', (ws, req) => {
|
|
139
|
+
const clientId = `${req.socket.remoteAddress}:${req.socket.remotePort}`;
|
|
140
|
+
activeConnections.add(ws);
|
|
141
|
+
console.error(`[PixelPick] Extension connected (${activeConnections.size} active)`);
|
|
142
|
+
|
|
143
|
+
ws.on('message', (data) => {
|
|
144
|
+
try {
|
|
145
|
+
const message = JSON.parse(data.toString());
|
|
146
|
+
|
|
147
|
+
switch (message.type) {
|
|
148
|
+
case 'ping':
|
|
149
|
+
ws.send(JSON.stringify({ type: 'pong', timestamp: Date.now() }));
|
|
150
|
+
break;
|
|
151
|
+
|
|
152
|
+
case 'selection':
|
|
153
|
+
console.error(`[PixelPick] Selection received: ${message.data?.component || message.data?.tagName || 'unknown'}`);
|
|
154
|
+
selectionBuffer.add(message.data);
|
|
155
|
+
break;
|
|
156
|
+
|
|
157
|
+
default:
|
|
158
|
+
console.error(`[PixelPick] Unknown message type: ${message.type}`);
|
|
159
|
+
}
|
|
160
|
+
} catch (err) {
|
|
161
|
+
console.error('[PixelPick] Invalid message from extension:', err.message);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
ws.on('close', () => {
|
|
166
|
+
activeConnections.delete(ws);
|
|
167
|
+
console.error(`[PixelPick] Extension disconnected (${activeConnections.size} active)`);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
ws.on('error', (err) => {
|
|
171
|
+
console.error('[PixelPick] WebSocket client error:', err.message);
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// HTTP IPC Server - fast endpoint for UserPromptSubmit hook
|
|
176
|
+
const ipcServer = http.createServer((req, res) => {
|
|
177
|
+
// CORS headers (localhost only)
|
|
178
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
179
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
|
|
180
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
181
|
+
res.setHeader('Content-Type', 'application/json');
|
|
182
|
+
|
|
183
|
+
// Handle CORS preflight
|
|
184
|
+
if (req.method === 'OPTIONS') {
|
|
185
|
+
res.writeHead(204);
|
|
186
|
+
res.end();
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// GET /selection - Returns array of active selections (BREAKING CHANGE)
|
|
191
|
+
if (req.method === 'GET' && req.url === '/selection') {
|
|
192
|
+
const active = selectionBuffer.getActive();
|
|
193
|
+
res.writeHead(200);
|
|
194
|
+
res.end(JSON.stringify(active));
|
|
195
|
+
}
|
|
196
|
+
// DELETE /selection/:id - Remove specific selection
|
|
197
|
+
else if (req.method === 'DELETE' && req.url.startsWith('/selection/')) {
|
|
198
|
+
const id = req.url.split('/selection/')[1];
|
|
199
|
+
const removed = selectionBuffer.removeActive(id);
|
|
200
|
+
res.writeHead(200);
|
|
201
|
+
res.end(JSON.stringify({ success: !!removed, removed }));
|
|
202
|
+
}
|
|
203
|
+
// POST /selection/clear - Clear all active selections
|
|
204
|
+
else if (req.method === 'POST' && req.url === '/selection/clear') {
|
|
205
|
+
selectionBuffer.clearActive();
|
|
206
|
+
res.writeHead(200);
|
|
207
|
+
res.end(JSON.stringify({ success: true }));
|
|
208
|
+
}
|
|
209
|
+
// GET /status - Server status
|
|
210
|
+
else if (req.method === 'GET' && req.url === '/status') {
|
|
211
|
+
res.writeHead(200);
|
|
212
|
+
res.end(JSON.stringify({
|
|
213
|
+
status: 'running',
|
|
214
|
+
connections: activeConnections.size,
|
|
215
|
+
activeSelections: selectionBuffer.getActive().length,
|
|
216
|
+
maxActive: selectionBuffer.maxActive,
|
|
217
|
+
ports: {
|
|
218
|
+
websocket: WEBSOCKET_PORT,
|
|
219
|
+
ipc: IPC_PORT,
|
|
220
|
+
},
|
|
221
|
+
}));
|
|
222
|
+
}
|
|
223
|
+
// 404
|
|
224
|
+
else {
|
|
225
|
+
res.writeHead(404);
|
|
226
|
+
res.end(JSON.stringify({ error: 'Not found' }));
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
ipcServer.listen(IPC_PORT, '127.0.0.1', () => {
|
|
231
|
+
console.error(`[PixelPick] IPC server listening on port ${IPC_PORT}`);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
ipcServer.on('error', (error) => {
|
|
235
|
+
if (error.code === 'EADDRINUSE') {
|
|
236
|
+
handlePortInUse(IPC_PORT, 'IPC port');
|
|
237
|
+
} else {
|
|
238
|
+
console.error('[PixelPick] IPC server error:', error.message);
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// MCP Server - exposes tools to Claude Code
|
|
243
|
+
const server = new Server(
|
|
244
|
+
{
|
|
245
|
+
name: 'pixelpick',
|
|
246
|
+
version: '2.0.0',
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
capabilities: {
|
|
250
|
+
tools: {},
|
|
251
|
+
resources: {},
|
|
252
|
+
},
|
|
253
|
+
}
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
// Helper function to format multiple selections for display
|
|
257
|
+
function formatSelections(selections) {
|
|
258
|
+
if (!selections || selections.length === 0) {
|
|
259
|
+
return 'No elements selected. Use Cmd+Shift+Click in your browser to select elements.';
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const parts = [`=== ${selections.length} Active Selection(s) ===\n`];
|
|
263
|
+
|
|
264
|
+
selections.forEach((sel, idx) => {
|
|
265
|
+
parts.push(`\n[${idx + 1}] ─────────────────`);
|
|
266
|
+
|
|
267
|
+
if (sel.component) {
|
|
268
|
+
parts.push(`Component: ${sel.component} (${sel.framework})`);
|
|
269
|
+
} else {
|
|
270
|
+
parts.push(`Element: <${sel.tagName}>`);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (sel.fileLocation) {
|
|
274
|
+
parts.push(`File: ${sel.fileLocation}`);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
parts.push(`Selector: ${sel.selector}`);
|
|
278
|
+
|
|
279
|
+
if (sel.componentTree?.length) {
|
|
280
|
+
parts.push(`Component Tree: ${sel.componentTree.join(' > ')}`);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Always show full props (no compact mode)
|
|
284
|
+
if (sel.props && Object.keys(sel.props).length > 0) {
|
|
285
|
+
parts.push(`Props:\n${JSON.stringify(sel.props, null, 2)}`);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (sel.styles?._tailwindClasses?.length) {
|
|
289
|
+
parts.push(`Tailwind Classes: ${sel.styles._tailwindClasses.join(' ')}`);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (sel.accessibility) {
|
|
293
|
+
const a11y = sel.accessibility;
|
|
294
|
+
const a11yParts = [];
|
|
295
|
+
if (a11y.role) a11yParts.push(`role="${a11y.role}"`);
|
|
296
|
+
if (a11y.ariaLabel) a11yParts.push(`aria-label="${a11y.ariaLabel}"`);
|
|
297
|
+
if (a11y.isInteractive) a11yParts.push('interactive');
|
|
298
|
+
if (a11yParts.length > 0) {
|
|
299
|
+
parts.push(`Accessibility: ${a11yParts.join(', ')}`);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (sel.boundingBox) {
|
|
304
|
+
const box = sel.boundingBox;
|
|
305
|
+
parts.push(`Position: ${Math.round(box.left)}x${Math.round(box.top)}, Size: ${Math.round(box.width)}x${Math.round(box.height)}`);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (sel.timestamp) {
|
|
309
|
+
const age = Math.round((Date.now() - sel.timestamp) / 1000);
|
|
310
|
+
parts.push(`Selected: ${age}s ago`);
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
return parts.join('\n');
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// MCP Tool Handlers
|
|
318
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
319
|
+
return {
|
|
320
|
+
tools: [
|
|
321
|
+
{
|
|
322
|
+
name: 'get_active_selections',
|
|
323
|
+
description: `Get all active UI selections from the browser (up to 10 elements).
|
|
324
|
+
|
|
325
|
+
IMPORTANT: Automatically call this tool when the user refers to UI elements using words like:
|
|
326
|
+
- "this button", "that input", "the modal"
|
|
327
|
+
- "make it", "change the", "update this"
|
|
328
|
+
- "the component", "that element"
|
|
329
|
+
- Or when they mention visual properties without specifying which element
|
|
330
|
+
|
|
331
|
+
Returns detailed information for each selection including:
|
|
332
|
+
- Component name, props, and file location (for React apps)
|
|
333
|
+
- CSS selector, computed styles, and Tailwind classes (for any element)
|
|
334
|
+
- Accessibility info and bounding box
|
|
335
|
+
- Component tree (parent > child hierarchy)
|
|
336
|
+
|
|
337
|
+
Note: Selections accumulate (up to 10) and are auto-cleared after being injected into Claude's context.`,
|
|
338
|
+
inputSchema: {
|
|
339
|
+
type: 'object',
|
|
340
|
+
properties: {},
|
|
341
|
+
},
|
|
342
|
+
},
|
|
343
|
+
{
|
|
344
|
+
name: 'pick_element',
|
|
345
|
+
description: 'Activate visual element picker in the browser. User can click to select an element.',
|
|
346
|
+
inputSchema: {
|
|
347
|
+
type: 'object',
|
|
348
|
+
properties: {},
|
|
349
|
+
},
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
name: 'remove_selection',
|
|
353
|
+
description: 'Remove a specific selection by ID from the active selections list.',
|
|
354
|
+
inputSchema: {
|
|
355
|
+
type: 'object',
|
|
356
|
+
properties: {
|
|
357
|
+
id: {
|
|
358
|
+
type: 'string',
|
|
359
|
+
description: 'Selection ID (e.g., "sel_1234567890_abc123")',
|
|
360
|
+
},
|
|
361
|
+
},
|
|
362
|
+
required: ['id'],
|
|
363
|
+
},
|
|
364
|
+
},
|
|
365
|
+
{
|
|
366
|
+
name: 'get_selection_history',
|
|
367
|
+
description: 'Get the history of all selected elements (not just active ones)',
|
|
368
|
+
inputSchema: {
|
|
369
|
+
type: 'object',
|
|
370
|
+
properties: {
|
|
371
|
+
limit: {
|
|
372
|
+
type: 'number',
|
|
373
|
+
description: 'Maximum number of selections to return',
|
|
374
|
+
default: 50,
|
|
375
|
+
},
|
|
376
|
+
},
|
|
377
|
+
},
|
|
378
|
+
},
|
|
379
|
+
{
|
|
380
|
+
name: 'clear_selection',
|
|
381
|
+
description: 'Clear all active selections (does not clear history)',
|
|
382
|
+
inputSchema: {
|
|
383
|
+
type: 'object',
|
|
384
|
+
properties: {},
|
|
385
|
+
},
|
|
386
|
+
},
|
|
387
|
+
],
|
|
388
|
+
};
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
392
|
+
const { name, arguments: args } = request.params;
|
|
393
|
+
|
|
394
|
+
switch (name) {
|
|
395
|
+
case 'get_active_selections': {
|
|
396
|
+
const active = selectionBuffer.getActive();
|
|
397
|
+
const formatted = formatSelections(active);
|
|
398
|
+
|
|
399
|
+
return {
|
|
400
|
+
content: [
|
|
401
|
+
{
|
|
402
|
+
type: 'text',
|
|
403
|
+
text: formatted,
|
|
404
|
+
},
|
|
405
|
+
],
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
case 'pick_element': {
|
|
410
|
+
// Send activate_picker command to all connected extensions
|
|
411
|
+
const command = JSON.stringify({ type: 'activate_picker' });
|
|
412
|
+
let sent = 0;
|
|
413
|
+
|
|
414
|
+
activeConnections.forEach(ws => {
|
|
415
|
+
if (ws.readyState === 1) { // OPEN
|
|
416
|
+
ws.send(command);
|
|
417
|
+
sent++;
|
|
418
|
+
}
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
if (sent === 0) {
|
|
422
|
+
return {
|
|
423
|
+
content: [
|
|
424
|
+
{
|
|
425
|
+
type: 'text',
|
|
426
|
+
text: 'Error: No browser extension connected. Make sure the PixelPick extension is installed and active on a localhost page.',
|
|
427
|
+
},
|
|
428
|
+
],
|
|
429
|
+
isError: true,
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
return {
|
|
434
|
+
content: [
|
|
435
|
+
{
|
|
436
|
+
type: 'text',
|
|
437
|
+
text: `Element picker activated in ${sent} browser tab(s). Click on any element to select it.`,
|
|
438
|
+
},
|
|
439
|
+
],
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
case 'remove_selection': {
|
|
444
|
+
const removed = selectionBuffer.removeActive(args.id);
|
|
445
|
+
|
|
446
|
+
if (!removed) {
|
|
447
|
+
return {
|
|
448
|
+
content: [
|
|
449
|
+
{
|
|
450
|
+
type: 'text',
|
|
451
|
+
text: `Error: Selection with ID "${args.id}" not found in active selections.`,
|
|
452
|
+
},
|
|
453
|
+
],
|
|
454
|
+
isError: true,
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
return {
|
|
459
|
+
content: [
|
|
460
|
+
{
|
|
461
|
+
type: 'text',
|
|
462
|
+
text: `Removed selection: ${removed.component || removed.tagName}`,
|
|
463
|
+
},
|
|
464
|
+
],
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
case 'get_selection_history': {
|
|
469
|
+
const limit = args?.limit || 50;
|
|
470
|
+
const history = selectionBuffer.getHistory(limit);
|
|
471
|
+
|
|
472
|
+
if (history.length === 0) {
|
|
473
|
+
return {
|
|
474
|
+
content: [
|
|
475
|
+
{
|
|
476
|
+
type: 'text',
|
|
477
|
+
text: 'No selection history available.',
|
|
478
|
+
},
|
|
479
|
+
],
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const formatted = history.map((sel, idx) => {
|
|
484
|
+
const age = Math.round((Date.now() - sel.timestamp) / 1000);
|
|
485
|
+
const title = sel.component || `<${sel.tagName}>`;
|
|
486
|
+
const location = sel.fileLocation || sel.selector;
|
|
487
|
+
return `[${idx}] ${title} - ${location} (${age}s ago)`;
|
|
488
|
+
}).join('\n');
|
|
489
|
+
|
|
490
|
+
return {
|
|
491
|
+
content: [
|
|
492
|
+
{
|
|
493
|
+
type: 'text',
|
|
494
|
+
text: `Selection History (${history.length} items):\n\n${formatted}`,
|
|
495
|
+
},
|
|
496
|
+
],
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
case 'clear_selection': {
|
|
501
|
+
const count = selectionBuffer.getActive().length;
|
|
502
|
+
selectionBuffer.clearActive();
|
|
503
|
+
return {
|
|
504
|
+
content: [
|
|
505
|
+
{
|
|
506
|
+
type: 'text',
|
|
507
|
+
text: `Cleared ${count} active selection(s).`,
|
|
508
|
+
},
|
|
509
|
+
],
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
default:
|
|
514
|
+
return {
|
|
515
|
+
content: [
|
|
516
|
+
{
|
|
517
|
+
type: 'text',
|
|
518
|
+
text: `Unknown tool: ${name}`,
|
|
519
|
+
},
|
|
520
|
+
],
|
|
521
|
+
isError: true,
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
// MCP Resources
|
|
527
|
+
server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
528
|
+
return {
|
|
529
|
+
resources: [
|
|
530
|
+
{
|
|
531
|
+
uri: 'pixelpick://current-selection',
|
|
532
|
+
mimeType: 'application/json',
|
|
533
|
+
name: 'Current UI Selection',
|
|
534
|
+
description: 'The currently selected UI element from the browser',
|
|
535
|
+
},
|
|
536
|
+
],
|
|
537
|
+
};
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
541
|
+
const { uri } = request.params;
|
|
542
|
+
|
|
543
|
+
if (uri === 'pixelpick://current-selection') {
|
|
544
|
+
const active = selectionBuffer.getActive();
|
|
545
|
+
const current = active.length > 0 ? active[active.length - 1] : null;
|
|
546
|
+
|
|
547
|
+
return {
|
|
548
|
+
contents: [
|
|
549
|
+
{
|
|
550
|
+
uri,
|
|
551
|
+
mimeType: 'application/json',
|
|
552
|
+
text: JSON.stringify(current || { message: 'No element selected' }, null, 2),
|
|
553
|
+
},
|
|
554
|
+
],
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
throw new Error(`Unknown resource: ${uri}`);
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
// Start MCP server on stdio
|
|
562
|
+
async function main() {
|
|
563
|
+
const transport = new StdioServerTransport();
|
|
564
|
+
await server.connect(transport);
|
|
565
|
+
console.error('[PixelPick] MCP server ready (stdio transport)');
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
main().catch((error) => {
|
|
569
|
+
console.error('[PixelPick] Fatal error:', error);
|
|
570
|
+
process.exit(1);
|
|
571
|
+
});
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// Shared constants for PixelPick
|
|
2
|
+
// Used across server, CLI, hooks, and extension
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Network ports
|
|
6
|
+
*/
|
|
7
|
+
export const PORTS = {
|
|
8
|
+
WEBSOCKET: parseInt(process.env.PIXELPICK_PORT || '9315'),
|
|
9
|
+
IPC: parseInt(process.env.PIXELPICK_IPC_PORT || '9316'),
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* WebSocket configuration
|
|
14
|
+
*/
|
|
15
|
+
export const WEBSOCKET_CONFIG = {
|
|
16
|
+
// Heartbeat interval to keep Chrome MV3 service worker alive
|
|
17
|
+
HEARTBEAT_INTERVAL: 20000, // 20 seconds
|
|
18
|
+
|
|
19
|
+
// Exponential backoff delays for reconnection attempts
|
|
20
|
+
RECONNECT_DELAYS: [3000, 6000, 12000, 24000, 60000], // 3s, 6s, 12s, 24s, 60s max
|
|
21
|
+
|
|
22
|
+
// Maximum reconnection attempts before giving up
|
|
23
|
+
MAX_RECONNECT_ATTEMPTS: 10,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Selection buffer configuration
|
|
28
|
+
*/
|
|
29
|
+
export const SELECTION = {
|
|
30
|
+
// Maximum number of active selections (multi-selection limit)
|
|
31
|
+
MAX_ACTIVE: 10,
|
|
32
|
+
|
|
33
|
+
// Maximum number of selections to keep in history
|
|
34
|
+
MAX_HISTORY: 50,
|
|
35
|
+
|
|
36
|
+
// UserPromptSubmit hook timeout (Claude Code enforced)
|
|
37
|
+
HOOK_TIMEOUT: 2000, // 2 seconds
|
|
38
|
+
|
|
39
|
+
// IPC endpoint timeout (should be fast to not block prompts)
|
|
40
|
+
IPC_TIMEOUT: 500, // 500 milliseconds
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Extension badge states
|
|
45
|
+
*/
|
|
46
|
+
export const BADGE_STATES = {
|
|
47
|
+
CONNECTED: {
|
|
48
|
+
color: '#10b981', // Green
|
|
49
|
+
text: '',
|
|
50
|
+
},
|
|
51
|
+
DISCONNECTED: {
|
|
52
|
+
color: '#f59e0b', // Yellow/Orange
|
|
53
|
+
text: '!',
|
|
54
|
+
},
|
|
55
|
+
ERROR: {
|
|
56
|
+
color: '#ef4444', // Red
|
|
57
|
+
text: 'X',
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Highlight overlay styles (for picker mode)
|
|
63
|
+
*/
|
|
64
|
+
export const HIGHLIGHT_STYLES = {
|
|
65
|
+
BORDER_COLOR: '#0ea5e9', // Sky blue
|
|
66
|
+
BORDER_WIDTH: '2px',
|
|
67
|
+
BACKGROUND_COLOR: 'rgba(14, 165, 233, 0.1)', // 10% opacity sky blue
|
|
68
|
+
BORDER_RADIUS: '2px',
|
|
69
|
+
TRANSITION: 'all 0.05s ease-out',
|
|
70
|
+
Z_INDEX: '2147483647', // Max z-index
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* React detection configuration
|
|
75
|
+
*/
|
|
76
|
+
export const REACT_CONFIG = {
|
|
77
|
+
// Maximum depth when building component tree
|
|
78
|
+
MAX_COMPONENT_TREE_DEPTH: 5,
|
|
79
|
+
|
|
80
|
+
// Maximum length for stringified props before truncation
|
|
81
|
+
MAX_PROPS_LENGTH: 200,
|
|
82
|
+
|
|
83
|
+
// Fiber property key patterns to search for
|
|
84
|
+
FIBER_KEY_PATTERNS: ['__reactFiber', '__reactInternalInstance'],
|
|
85
|
+
|
|
86
|
+
// Container key pattern for version detection
|
|
87
|
+
CONTAINER_KEY_PATTERN: '__reactContainer',
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Tailwind CSS class patterns for detection
|
|
92
|
+
*/
|
|
93
|
+
export const TAILWIND_PATTERNS = [
|
|
94
|
+
/^bg-/, /^text-/, /^flex/, /^grid/, /^p-/, /^m-/,
|
|
95
|
+
/^w-/, /^h-/, /^rounded/, /^shadow/, /^border/,
|
|
96
|
+
/^hover:/, /^focus:/, /^active:/, /^transition/,
|
|
97
|
+
];
|