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.
@@ -0,0 +1,225 @@
1
+ // React component detection via Fiber introspection
2
+ import { REACT_CONFIG } from '../constants.js';
3
+
4
+ /**
5
+ * Detects which framework (if any) is used by the element
6
+ * @param {HTMLElement} element
7
+ * @returns {string|null} - Framework name or null
8
+ */
9
+ export function detectFramework(element) {
10
+ // React: Check for __reactFiber$ or __reactInternalInstance$ property
11
+ const fiberKey = Object.keys(element).find(k =>
12
+ REACT_CONFIG.FIBER_KEY_PATTERNS.some(pattern => k.startsWith(pattern))
13
+ );
14
+
15
+ if (fiberKey) return 'react';
16
+
17
+ // Future: Vue, Svelte, etc.
18
+ return null;
19
+ }
20
+
21
+ /**
22
+ * Gets the component name from a React fiber
23
+ * @param {Object} fiber - React fiber object
24
+ * @returns {string|null}
25
+ */
26
+ export function getComponentName(fiber) {
27
+ if (!fiber || !fiber.type) return null;
28
+
29
+ // Function component or class component
30
+ if (typeof fiber.type === 'function') {
31
+ return fiber.type.displayName || fiber.type.name || 'Anonymous';
32
+ }
33
+
34
+ return null;
35
+ }
36
+
37
+ /**
38
+ * Detects React version
39
+ * @returns {string} - React version or 'unknown'
40
+ */
41
+ export function getReactVersion() {
42
+ try {
43
+ // Check for React in window
44
+ if (window.React) {
45
+ const internals = window.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;
46
+ return internals?.ReactVersion || 'unknown';
47
+ }
48
+
49
+ // Try to detect from fiber
50
+ const rootElements = document.querySelectorAll('[id*="root"]');
51
+ for (const root of rootElements) {
52
+ const fiberKey = Object.keys(root).find(k =>
53
+ k.startsWith(REACT_CONFIG.CONTAINER_KEY_PATTERN)
54
+ );
55
+ if (fiberKey) {
56
+ // Heuristic: React 18+ uses __reactContainer
57
+ return '18+';
58
+ }
59
+ }
60
+
61
+ return 'unknown';
62
+ } catch {
63
+ return 'unknown';
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Sanitizes React props for safe serialization
69
+ * @param {Object} props - React component props
70
+ * @returns {Object|null}
71
+ */
72
+ export function sanitizeProps(props) {
73
+ if (!props || typeof props !== 'object') return null;
74
+
75
+ const sanitized = {};
76
+
77
+ for (const [key, value] of Object.entries(props)) {
78
+ // Skip children (too large)
79
+ if (key === 'children') continue;
80
+
81
+ try {
82
+ // Handle functions
83
+ if (typeof value === 'function') {
84
+ sanitized[key] = `[Function: ${value.name || 'anonymous'}]`;
85
+ continue;
86
+ }
87
+
88
+ // Handle symbols
89
+ if (typeof value === 'symbol') {
90
+ continue;
91
+ }
92
+
93
+ // Handle objects (with truncation)
94
+ if (typeof value === 'object' && value !== null) {
95
+ const str = JSON.stringify(value);
96
+ if (str.length > REACT_CONFIG.MAX_PROPS_LENGTH) {
97
+ sanitized[key] = str.slice(0, REACT_CONFIG.MAX_PROPS_LENGTH) + '...';
98
+ } else {
99
+ sanitized[key] = value;
100
+ }
101
+ continue;
102
+ }
103
+
104
+ // Primitives
105
+ sanitized[key] = value;
106
+ } catch (err) {
107
+ // Circular reference or other error
108
+ sanitized[key] = '[Object]';
109
+ }
110
+ }
111
+
112
+ return Object.keys(sanitized).length > 0 ? sanitized : null;
113
+ }
114
+
115
+ /**
116
+ * Builds component tree by walking up the fiber tree
117
+ * @param {Object} fiber - Starting React fiber
118
+ * @param {number} maxDepth - Maximum depth to traverse
119
+ * @returns {Array<string>|null}
120
+ */
121
+ export function buildComponentTree(fiber, maxDepth = REACT_CONFIG.MAX_COMPONENT_TREE_DEPTH) {
122
+ const tree = [];
123
+ let current = fiber;
124
+ let depth = 0;
125
+
126
+ while (current && depth < maxDepth) {
127
+ if (typeof current.type !== 'string') {
128
+ const name = getComponentName(current);
129
+ if (name) {
130
+ tree.push(name);
131
+ }
132
+ }
133
+
134
+ current = current.return;
135
+ depth++;
136
+ }
137
+
138
+ return tree.length > 0 ? tree.reverse() : null;
139
+ }
140
+
141
+ /**
142
+ * Extracts React component data from a DOM element
143
+ * @param {HTMLElement} element
144
+ * @returns {Object|null} - Component data or null
145
+ */
146
+ export function extractReactData(element) {
147
+ try {
148
+ // Find fiber key
149
+ const fiberKey = Object.keys(element).find(k =>
150
+ REACT_CONFIG.FIBER_KEY_PATTERNS.some(pattern => k.startsWith(pattern))
151
+ );
152
+
153
+ if (!fiberKey) {
154
+ return null;
155
+ }
156
+
157
+ const fiber = element[fiberKey];
158
+ if (!fiber) return null;
159
+
160
+ // Walk up to find component fiber (skip host fibers like 'div')
161
+ let componentFiber = fiber;
162
+ while (componentFiber && typeof componentFiber.type === 'string') {
163
+ componentFiber = componentFiber.return;
164
+ if (!componentFiber) break;
165
+ }
166
+
167
+ if (!componentFiber) {
168
+ return {
169
+ name: null,
170
+ props: null,
171
+ fileLocation: null,
172
+ tree: null,
173
+ };
174
+ }
175
+
176
+ // Extract component name
177
+ const name = getComponentName(componentFiber);
178
+
179
+ // Extract props
180
+ const props = sanitizeProps(componentFiber.memoizedProps);
181
+
182
+ // Extract file location
183
+ let fileLocation = null;
184
+
185
+ // Try _debugSource (React 18)
186
+ if (componentFiber._debugSource) {
187
+ const src = componentFiber._debugSource;
188
+ fileLocation = `${src.fileName}:${src.lineNumber}:${src.columnNumber}`;
189
+ } else {
190
+ // Fallback: Parse stack trace (React 19)
191
+ try {
192
+ const stack = new Error().stack;
193
+ const lines = stack.split('\n');
194
+ // Find first non-node_modules line
195
+ for (const line of lines) {
196
+ if (line.includes('.jsx') || line.includes('.tsx') || line.includes('.js')) {
197
+ if (!line.includes('node_modules') && !line.includes('inspector.js')) {
198
+ // Extract file:line from stack trace
199
+ const match = line.match(/([^/\s]+\.(jsx?|tsx?)):(\d+):(\d+)/);
200
+ if (match) {
201
+ fileLocation = `${match[1]}:${match[3]}`;
202
+ break;
203
+ }
204
+ }
205
+ }
206
+ }
207
+ } catch {
208
+ // Stack parsing failed
209
+ }
210
+ }
211
+
212
+ // Build component tree
213
+ const tree = buildComponentTree(componentFiber);
214
+
215
+ return {
216
+ name,
217
+ props,
218
+ fileLocation,
219
+ tree,
220
+ };
221
+ } catch (err) {
222
+ console.error('[PixelPick] React extraction error:', err);
223
+ return null;
224
+ }
225
+ }
package/package.json ADDED
@@ -0,0 +1,75 @@
1
+ {
2
+ "name": "pixelpick",
3
+ "version": "2.0.0",
4
+ "description": "Visual UI inspector for Claude Code via MCP",
5
+ "type": "module",
6
+ "bin": {
7
+ "pixelpick": "src/cli/index.js",
8
+ "pixelpick-server": "src/server/index.js",
9
+ "pixelpick-check-selection": "src/hooks/check-selection.mjs"
10
+ },
11
+ "files": [
12
+ "src/",
13
+ "dist/chrome-mv3/",
14
+ "public/icon/",
15
+ "README.md",
16
+ "LICENSE"
17
+ ],
18
+ "scripts": {
19
+ "dev": "wxt",
20
+ "build": "wxt build",
21
+ "build:firefox": "wxt build --browser firefox",
22
+ "zip": "wxt zip",
23
+ "start": "node src/server/index.js",
24
+ "test": "node --test tests/**/*.test.js",
25
+ "serve-test": "node tests/helpers/serve-test.js",
26
+ "test:react": "npm run test:ensure-deps && cd tests/react-app && npm run dev",
27
+ "test:ensure-deps": "[ -d tests/react-app/node_modules ] || (echo '\nInstalling React test app dependencies...\n' && cd tests/react-app && npm install)",
28
+ "test:setup": "cd tests/react-app && npm install",
29
+ "generate-icons": "node scripts/generate-icons.mjs",
30
+ "prepublishOnly": "wxt build",
31
+ "postinstall": "chmod +x src/cli/index.js src/server/index.js src/hooks/check-selection.mjs 2>/dev/null || true"
32
+ },
33
+ "dependencies": {
34
+ "@modelcontextprotocol/sdk": "^1.0.0",
35
+ "ws": "^8.16.0"
36
+ },
37
+ "engines": {
38
+ "node": ">=18.0.0"
39
+ },
40
+ "keywords": [
41
+ "mcp",
42
+ "claude",
43
+ "claude-code",
44
+ "model-context-protocol",
45
+ "ui-inspector",
46
+ "developer-tools",
47
+ "chrome-extension",
48
+ "react"
49
+ ],
50
+ "license": "MIT",
51
+ "author": "bilune",
52
+ "repository": {
53
+ "type": "git",
54
+ "url": "git+https://github.com/bilune/pixelpick.git"
55
+ },
56
+ "bugs": {
57
+ "url": "https://github.com/bilune/pixelpick/issues"
58
+ },
59
+ "homepage": "https://github.com/bilune/pixelpick#readme",
60
+ "devDependencies": {
61
+ "@tailwindcss/postcss": "^4.1.18",
62
+ "@types/chrome": "^0.1.36",
63
+ "@types/react": "^19.2.14",
64
+ "@types/react-dom": "^19.2.3",
65
+ "@wxt-dev/module-react": "^1.1.5",
66
+ "autoprefixer": "^10.4.24",
67
+ "lucide-react": "^0.564.0",
68
+ "postcss": "^8.5.6",
69
+ "react": "^19.2.4",
70
+ "react-dom": "^19.2.4",
71
+ "tailwindcss": "^4.0.0",
72
+ "typescript": "^5.9.3",
73
+ "wxt": "^0.20.17"
74
+ }
75
+ }
Binary file
Binary file
Binary file
@@ -0,0 +1,278 @@
1
+ #!/usr/bin/env node
2
+ import { execSync } from 'child_process';
3
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
4
+ import { join, dirname } from 'path';
5
+ import { fileURLToPath } from 'url';
6
+
7
+ const __dirname = dirname(fileURLToPath(import.meta.url));
8
+ const command = process.argv[2];
9
+
10
+ // Flip to true once Chrome Web Store listing is approved
11
+ const WEB_STORE_AVAILABLE = false;
12
+ const WEB_STORE_URL = ''; // e.g. https://chrome.google.com/webstore/detail/pixelpick/...
13
+
14
+ switch (command) {
15
+ case 'init':
16
+ init();
17
+ break;
18
+ case 'start':
19
+ case undefined:
20
+ startServer();
21
+ break;
22
+ case 'status':
23
+ checkStatus();
24
+ break;
25
+ case 'help':
26
+ case '--help':
27
+ case '-h':
28
+ showHelp();
29
+ break;
30
+ case '--version':
31
+ case '-v':
32
+ showVersion();
33
+ break;
34
+ default:
35
+ console.error(`Unknown command: ${command}\n`);
36
+ showHelp();
37
+ process.exit(1);
38
+ }
39
+
40
+ function init() {
41
+ const isProjectScope = process.argv.includes('--project');
42
+ const scope = isProjectScope ? 'project' : 'user';
43
+
44
+ console.log('PixelPick -- Setting up Claude Code integration...\n');
45
+
46
+ // Step 0: Check prerequisites
47
+ console.log('[1/4] Checking prerequisites...');
48
+ try {
49
+ execSync('claude --version', { stdio: 'pipe' });
50
+ console.log(' Claude CLI found.\n');
51
+ } catch {
52
+ console.error(' Claude CLI not found.\n');
53
+ console.error(' Install it first:');
54
+ console.error(' npm install -g @anthropic-ai/claude-code\n');
55
+ console.error(' Then run this command again: npx pixelpick init');
56
+ process.exit(1);
57
+ }
58
+
59
+ // Step 1: Register MCP server
60
+ console.log('[2/4] Registering MCP server...');
61
+ try {
62
+ const serverPath = join(__dirname, '../server/index.js');
63
+ try {
64
+ execSync(
65
+ `claude mcp add --scope ${scope} pixelpick -- node "${serverPath}"`,
66
+ { stdio: 'pipe' }
67
+ );
68
+ console.log(' MCP server registered.\n');
69
+ } catch (addErr) {
70
+ // Check if it already exists
71
+ const listOutput = execSync('claude mcp list', { encoding: 'utf8', stdio: 'pipe' });
72
+ if (listOutput.includes('pixelpick')) {
73
+ console.log(' MCP server already registered (skipped).\n');
74
+ } else {
75
+ throw addErr;
76
+ }
77
+ }
78
+ } catch (err) {
79
+ console.error(' Failed to register MCP server.');
80
+ console.error(' Error:', err.message);
81
+ process.exit(1);
82
+ }
83
+
84
+ // Step 2: Add UserPromptSubmit hook
85
+ console.log('[3/4] Installing selection injection hook...');
86
+ const settingsPath = isProjectScope
87
+ ? join(process.cwd(), '.claude', 'settings.json')
88
+ : join(process.env.HOME, '.claude', 'settings.json');
89
+
90
+ try {
91
+ const dir = dirname(settingsPath);
92
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
93
+
94
+ let settings = {};
95
+ if (existsSync(settingsPath)) {
96
+ settings = JSON.parse(readFileSync(settingsPath, 'utf8'));
97
+ }
98
+
99
+ if (!settings.hooks) settings.hooks = {};
100
+ if (!settings.hooks.UserPromptSubmit) settings.hooks.UserPromptSubmit = [];
101
+
102
+ // Check if hook already exists
103
+ const hookExists = settings.hooks.UserPromptSubmit.some(
104
+ h => h.hooks?.some(hook => hook.command?.includes('pixelpick'))
105
+ );
106
+
107
+ if (!hookExists) {
108
+ const checkSelectionPath = join(__dirname, '../hooks/check-selection.mjs');
109
+ settings.hooks.UserPromptSubmit.push({
110
+ hooks: [{
111
+ type: 'command',
112
+ command: `node "${checkSelectionPath}"`,
113
+ timeout: 2000
114
+ }]
115
+ });
116
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
117
+ console.log(' Hook installed.\n');
118
+ } else {
119
+ console.log(' Hook already installed (skipped).\n');
120
+ }
121
+ } catch (err) {
122
+ console.error(' Failed to install hook:', err.message);
123
+ process.exit(1);
124
+ }
125
+
126
+ // Step 3: Chrome Extension guidance
127
+ console.log('[4/4] Chrome Extension...');
128
+
129
+ if (WEB_STORE_AVAILABLE) {
130
+ console.log(` Install from Chrome Web Store:`);
131
+ console.log(` ${WEB_STORE_URL}\n`);
132
+ } else {
133
+ // Find the built extension in the package
134
+ const extensionPath = join(__dirname, '../../dist/chrome-mv3');
135
+
136
+ if (existsSync(extensionPath)) {
137
+ console.log(' Load the extension manually:\n');
138
+ console.log(` 1. Open chrome://extensions`);
139
+ console.log(` 2. Enable "Developer mode" (top right)`);
140
+ console.log(` 3. Click "Load unpacked"`);
141
+ console.log(` 4. Select: ${extensionPath}\n`);
142
+
143
+ // Try to copy path to clipboard (macOS)
144
+ try {
145
+ execSync(`echo "${extensionPath}" | pbcopy`, { stdio: 'pipe' });
146
+ console.log(' (Extension path copied to clipboard)\n');
147
+ } catch {
148
+ // Silent fail -- pbcopy not available on Linux/Windows
149
+ }
150
+
151
+ // Try to open chrome://extensions
152
+ try {
153
+ execSync('open "chrome://extensions"', { stdio: 'pipe' });
154
+ } catch {
155
+ // Silent fail -- not macOS or Chrome not default browser
156
+ }
157
+ } else {
158
+ console.log(' Extension build not found. Build it first:\n');
159
+ console.log(' cd node_modules/pixelpick && npm run build\n');
160
+ console.log(' Then load dist/chrome-mv3/ as unpacked extension.');
161
+ console.log(' See: https://github.com/bilune/pixelpick#setup\n');
162
+ }
163
+ }
164
+
165
+ // Done
166
+ console.log('--- Setup complete! ---\n');
167
+ console.log('How to use:');
168
+ console.log(' 1. Open your app in Chrome (localhost)');
169
+ console.log(' 2. Cmd+Shift+Click on any element');
170
+ console.log(' 3. Switch to Claude Code and describe your change');
171
+ console.log(' 4. Claude automatically sees what you selected\n');
172
+ console.log('Verify: npx pixelpick status');
173
+ }
174
+
175
+ function startServer() {
176
+ import('../server/index.js');
177
+ }
178
+
179
+ async function checkStatus() {
180
+ console.log('PixelPick Status');
181
+ console.log('-'.repeat(42) + '\n');
182
+
183
+ // Check MCP server
184
+ let serverRunning = false;
185
+
186
+ try {
187
+ const response = await fetch('http://127.0.0.1:9316/status');
188
+ const data = await response.json();
189
+ serverRunning = true;
190
+ console.log(`MCP Server: Running (localhost:${data.ports.websocket})`);
191
+ console.log(`Active Connections: ${data.connections}`);
192
+
193
+ // Get active selections
194
+ const selectionResponse = await fetch('http://127.0.0.1:9316/selection');
195
+ const selections = await selectionResponse.json();
196
+ if (Array.isArray(selections) && selections.length > 0) {
197
+ console.log(`Active Selections: ${selections.length}`);
198
+ selections.forEach((sel, idx) => {
199
+ const label = sel.component
200
+ ? `${sel.component} (${sel.framework})`
201
+ : `<${sel.tagName}>`;
202
+ const age = Math.round((Date.now() - sel.timestamp) / 1000);
203
+ console.log(` [${idx + 1}] ${label} - ${age}s ago`);
204
+ });
205
+ } else {
206
+ console.log('Active Selections: None');
207
+ }
208
+ } catch {
209
+ console.log('MCP Server: Not running');
210
+ console.log(' Run: npx pixelpick start');
211
+ }
212
+
213
+ // Check Claude Code registration
214
+ try {
215
+ const result = execSync('claude mcp list', { encoding: 'utf8', stdio: 'pipe' });
216
+ if (result.includes('pixelpick')) {
217
+ console.log('Claude Code: Registered');
218
+ } else {
219
+ console.log('Claude Code: Not registered');
220
+ console.log(' Run: npx pixelpick init');
221
+ }
222
+ } catch {
223
+ console.log('Claude Code: Could not check');
224
+ }
225
+
226
+ // Check hook
227
+ try {
228
+ const settingsPath = join(process.env.HOME, '.claude', 'settings.json');
229
+ if (existsSync(settingsPath)) {
230
+ const settings = JSON.parse(readFileSync(settingsPath, 'utf8'));
231
+ const hasHook = settings.hooks?.UserPromptSubmit?.some(
232
+ h => h.hooks?.some(hook => hook.command?.includes('pixelpick'))
233
+ );
234
+ if (hasHook) {
235
+ console.log('UserPrompt Hook: Installed');
236
+ } else {
237
+ console.log('UserPrompt Hook: Not installed');
238
+ console.log(' Run: npx pixelpick init');
239
+ }
240
+ }
241
+ } catch {
242
+ console.log('UserPrompt Hook: Could not check');
243
+ }
244
+
245
+ console.log();
246
+ }
247
+
248
+ function showHelp() {
249
+ console.log(`
250
+ PixelPick -- Visual UI inspector for Claude Code
251
+
252
+ Commands:
253
+ pixelpick init Set up MCP server, hooks, and extension
254
+ pixelpick start Start the MCP server
255
+ pixelpick status Check connection status
256
+ pixelpick help Show this help message
257
+
258
+ Options:
259
+ --project Use project scope instead of user scope (for init)
260
+ --version, -v Show version number
261
+
262
+ Environment Variables:
263
+ PIXELPICK_PORT WebSocket port (default: 9315)
264
+ PIXELPICK_IPC_PORT HTTP IPC port (default: 9316)
265
+
266
+ More info: https://github.com/bilune/pixelpick
267
+ `);
268
+ }
269
+
270
+ function showVersion() {
271
+ try {
272
+ const pkgPath = join(__dirname, '../../package.json');
273
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
274
+ console.log(`pixelpick v${pkg.version}`);
275
+ } catch {
276
+ console.log('pixelpick (version unknown)');
277
+ }
278
+ }
@@ -0,0 +1,123 @@
1
+ #!/usr/bin/env node
2
+ import http from 'http';
3
+ import { PORTS, SELECTION } from '../shared/constants.js';
4
+
5
+ const IPC_PORT = PORTS.IPC;
6
+
7
+ async function checkSelection() {
8
+ return new Promise((resolve) => {
9
+ const req = http.get(
10
+ `http://127.0.0.1:${IPC_PORT}/selection`,
11
+ { timeout: SELECTION.IPC_TIMEOUT }, // Fast timeout - must not delay prompts
12
+ (res) => {
13
+ let data = '';
14
+ res.on('data', chunk => data += chunk);
15
+ res.on('end', () => {
16
+ try {
17
+ const selections = JSON.parse(data);
18
+ resolve(selections);
19
+ } catch {
20
+ resolve(null);
21
+ }
22
+ });
23
+ }
24
+ );
25
+ req.on('error', () => resolve(null)); // Server not running = no selections
26
+ req.on('timeout', () => { req.destroy(); resolve(null); });
27
+ });
28
+ }
29
+
30
+ async function clearSelections() {
31
+ return new Promise((resolve) => {
32
+ const postData = '';
33
+ const options = {
34
+ hostname: '127.0.0.1',
35
+ port: IPC_PORT,
36
+ path: '/selection/clear',
37
+ method: 'POST',
38
+ headers: {
39
+ 'Content-Type': 'application/json',
40
+ 'Content-Length': Buffer.byteLength(postData),
41
+ },
42
+ timeout: SELECTION.IPC_TIMEOUT,
43
+ };
44
+
45
+ const req = http.request(options, (res) => {
46
+ // Silent success
47
+ resolve(true);
48
+ });
49
+
50
+ req.on('error', () => resolve(false)); // Silent failure
51
+ req.on('timeout', () => { req.destroy(); resolve(false); });
52
+ req.write(postData);
53
+ req.end();
54
+ });
55
+ }
56
+
57
+ async function main() {
58
+ const selections = await checkSelection();
59
+
60
+ // If no selections or invalid data, exit silently
61
+ if (!selections || !Array.isArray(selections) || selections.length === 0) {
62
+ process.exit(0);
63
+ return;
64
+ }
65
+
66
+ // Build formatted context for multiple selections
67
+ const parts = ['[PixelPick — Active Browser Selections]'];
68
+
69
+ selections.forEach((selection, idx) => {
70
+ parts.push(`\n[${idx + 1}] ─────────────────`);
71
+
72
+ if (selection.component) {
73
+ parts.push(`Component: ${selection.component} (${selection.framework})`);
74
+ } else {
75
+ parts.push(`Element: <${selection.tagName}>`);
76
+ }
77
+
78
+ if (selection.fileLocation) {
79
+ parts.push(`File: ${selection.fileLocation}`);
80
+ }
81
+
82
+ parts.push(`Selector: ${selection.selector}`);
83
+
84
+ if (selection.componentTree?.length) {
85
+ parts.push(`Tree: ${selection.componentTree.join(' > ')}`);
86
+ }
87
+
88
+ // Always show full props (no compact mode)
89
+ if (selection.props && Object.keys(selection.props).length > 0) {
90
+ parts.push(`Props: ${JSON.stringify(selection.props, null, 0)}`);
91
+ }
92
+
93
+ if (selection.styles?._tailwindClasses?.length) {
94
+ parts.push(`Tailwind: ${selection.styles._tailwindClasses.join(' ')}`);
95
+ }
96
+
97
+ if (selection.accessibility?.isInteractive) {
98
+ const a11y = [];
99
+ if (selection.accessibility.role) a11y.push(`role="${selection.accessibility.role}"`);
100
+ if (selection.accessibility.ariaLabel) a11y.push(`aria-label="${selection.accessibility.ariaLabel}"`);
101
+ if (a11y.length > 0) {
102
+ parts.push(`A11y: ${a11y.join(', ')}`);
103
+ }
104
+ }
105
+ });
106
+
107
+ const context = parts.join('\n');
108
+
109
+ // Output as JSON with additionalContext
110
+ const output = {
111
+ hookSpecificOutput: {
112
+ hookEventName: 'UserPromptSubmit',
113
+ additionalContext: context
114
+ }
115
+ };
116
+
117
+ process.stdout.write(JSON.stringify(output));
118
+
119
+ // Auto-clear selections after injection (fire-and-forget)
120
+ clearSelections().then(() => process.exit(0));
121
+ }
122
+
123
+ main();