kiro-mobile-bridge 1.0.7 → 1.0.10

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,214 @@
1
+ /**
2
+ * Message injection service - sends messages to Kiro chat via CDP
3
+ */
4
+ import { escapeForJavaScript, validateMessage } from '../utils/security.js';
5
+
6
+ /**
7
+ * Create script to inject message into chat input
8
+ * @param {string} messageText - Message to inject
9
+ * @returns {string} - JavaScript expression
10
+ */
11
+ function createInjectScript(messageText) {
12
+ // Use proper escaping to prevent XSS and injection attacks
13
+ const escaped = escapeForJavaScript(messageText);
14
+
15
+ return `(async () => {
16
+ const text = '${escaped}';
17
+ let targetDoc = document;
18
+ const activeFrame = document.getElementById('active-frame');
19
+ if (activeFrame && activeFrame.contentDocument) targetDoc = activeFrame.contentDocument;
20
+
21
+ // Find the CHAT INPUT editor specifically (not any contenteditable in the page)
22
+ // Look for the input area at the bottom of the chat, not message bubbles
23
+ let editor = null;
24
+
25
+ // Strategy 1: Find by common chat input container patterns
26
+ const inputContainerSelectors = [
27
+ '[class*="chat-input"]',
28
+ '[class*="message-input"]',
29
+ '[class*="composer"]',
30
+ '[class*="input-area"]',
31
+ '[class*="InputArea"]',
32
+ 'form[class*="chat"]',
33
+ '[data-testid*="input"]'
34
+ ];
35
+
36
+ for (const containerSel of inputContainerSelectors) {
37
+ const container = targetDoc.querySelector(containerSel);
38
+ if (container) {
39
+ const editorInContainer = container.querySelector('.tiptap.ProseMirror[contenteditable="true"], [data-lexical-editor="true"][contenteditable="true"], [contenteditable="true"], textarea');
40
+ if (editorInContainer && editorInContainer.offsetParent !== null) {
41
+ editor = editorInContainer;
42
+ break;
43
+ }
44
+ }
45
+ }
46
+
47
+ // Strategy 2: Find TipTap/ProseMirror editors that are visible and near a submit button
48
+ if (!editor) {
49
+ const allEditors = [...targetDoc.querySelectorAll('.tiptap.ProseMirror[contenteditable="true"]')].filter(el => el.offsetParent !== null);
50
+ for (const ed of allEditors) {
51
+ // Check if there's a submit button nearby (sibling or in same parent form)
52
+ const parent = ed.closest('form') || ed.parentElement?.parentElement?.parentElement;
53
+ if (parent) {
54
+ const hasSubmit = parent.querySelector('button[data-variant="submit"], button[type="submit"], svg.lucide-arrow-right');
55
+ if (hasSubmit) { editor = ed; break; }
56
+ }
57
+ }
58
+ // Fallback to last visible TipTap editor
59
+ if (!editor && allEditors.length > 0) editor = allEditors.at(-1);
60
+ }
61
+
62
+ // Strategy 3: Find Lexical editors
63
+ if (!editor) {
64
+ const lexicalEditors = [...targetDoc.querySelectorAll('[data-lexical-editor="true"][contenteditable="true"]')].filter(el => el.offsetParent !== null);
65
+ for (const ed of lexicalEditors) {
66
+ const parent = ed.closest('form') || ed.parentElement?.parentElement?.parentElement;
67
+ if (parent) {
68
+ const hasSubmit = parent.querySelector('button[data-variant="submit"], button[type="submit"]');
69
+ if (hasSubmit) { editor = ed; break; }
70
+ }
71
+ }
72
+ if (!editor && lexicalEditors.length > 0) editor = lexicalEditors.at(-1);
73
+ }
74
+
75
+ // Strategy 4: Generic contenteditable (last resort)
76
+ if (!editor) {
77
+ const editables = [...targetDoc.querySelectorAll('[contenteditable="true"]')].filter(el => el.offsetParent !== null);
78
+ editor = editables.at(-1);
79
+ }
80
+
81
+ // Strategy 5: Textarea fallback
82
+ if (!editor) {
83
+ const textareas = [...targetDoc.querySelectorAll('textarea')].filter(el => el.offsetParent !== null);
84
+ editor = textareas.at(-1);
85
+ }
86
+
87
+ if (!editor) return { ok: false, error: 'editor_not_found' };
88
+
89
+ const isTextarea = editor.tagName.toLowerCase() === 'textarea';
90
+ const isProseMirror = editor.classList.contains('ProseMirror') || editor.classList.contains('tiptap');
91
+ const isLexical = editor.hasAttribute('data-lexical-editor');
92
+
93
+ // Focus the editor first
94
+ editor.focus();
95
+ await new Promise(r => setTimeout(r, 50));
96
+
97
+ if (isTextarea) {
98
+ // Textarea: simple value assignment
99
+ editor.value = text;
100
+ editor.dispatchEvent(new Event('input', { bubbles: true }));
101
+ } else if (isProseMirror) {
102
+ // ProseMirror/TipTap: Clear and insert via DOM manipulation
103
+ // This works because TipTap syncs DOM changes to its state
104
+ editor.innerHTML = '';
105
+ const p = targetDoc.createElement('p');
106
+ p.textContent = text;
107
+ editor.appendChild(p);
108
+ editor.dispatchEvent(new InputEvent('input', { bubbles: true, inputType: 'insertText', data: text }));
109
+ } else if (isLexical) {
110
+ // Lexical: Must use execCommand to properly update internal state
111
+ // Lexical listens to beforeinput/input events from execCommand
112
+
113
+ // First, select all content
114
+ const selection = targetDoc.getSelection();
115
+ const range = targetDoc.createRange();
116
+ range.selectNodeContents(editor);
117
+ selection.removeAllRanges();
118
+ selection.addRange(range);
119
+
120
+ // Delete existing content
121
+ targetDoc.execCommand('delete', false, null);
122
+
123
+ // Wait for Lexical to process the deletion
124
+ await new Promise(r => setTimeout(r, 30));
125
+
126
+ // Insert new text using execCommand (Lexical intercepts this)
127
+ const inserted = targetDoc.execCommand('insertText', false, text);
128
+
129
+ if (!inserted) {
130
+ // Fallback: Try setting textContent and dispatching events
131
+ editor.textContent = text;
132
+ editor.dispatchEvent(new InputEvent('beforeinput', { bubbles: true, cancelable: true, inputType: 'insertText', data: text }));
133
+ editor.dispatchEvent(new InputEvent('input', { bubbles: true, inputType: 'insertText', data: text }));
134
+ }
135
+ } else {
136
+ // Generic contenteditable
137
+ const selection = targetDoc.getSelection();
138
+ const range = targetDoc.createRange();
139
+ range.selectNodeContents(editor);
140
+ selection.removeAllRanges();
141
+ selection.addRange(range);
142
+
143
+ targetDoc.execCommand('delete', false, null);
144
+
145
+ let inserted = false;
146
+ try { inserted = !!targetDoc.execCommand('insertText', false, text); } catch (e) {}
147
+
148
+ if (!inserted) {
149
+ editor.textContent = text;
150
+ editor.dispatchEvent(new InputEvent('input', { bubbles: true, inputType: 'insertText', data: text }));
151
+ }
152
+ }
153
+
154
+ // Wait for editor state to sync
155
+ await new Promise(r => requestAnimationFrame(() => requestAnimationFrame(r)));
156
+ await new Promise(r => setTimeout(r, 100));
157
+
158
+ // Find and click submit button
159
+ const submitButton = targetDoc.querySelector('button[data-variant="submit"]:not([disabled])') ||
160
+ targetDoc.querySelector('svg.lucide-arrow-right')?.closest('button:not([disabled])') ||
161
+ targetDoc.querySelector('button[type="submit"]:not([disabled])') ||
162
+ targetDoc.querySelector('button[aria-label*="send" i]:not([disabled])');
163
+
164
+ if (submitButton) {
165
+ submitButton.click();
166
+ return { ok: true, method: 'click_submit' };
167
+ }
168
+
169
+ // Fallback: Enter key
170
+ editor.dispatchEvent(new KeyboardEvent('keydown', { bubbles: true, cancelable: true, key: 'Enter', code: 'Enter', keyCode: 13 }));
171
+ return { ok: true, method: 'enter_key' };
172
+ })()`;
173
+ }
174
+
175
+ /**
176
+ * Inject a message into the chat via CDP
177
+ * @param {CDPConnection} cdp - CDP connection
178
+ * @param {string} message - Message text
179
+ * @returns {Promise<{success: boolean, method?: string, error?: string}>}
180
+ */
181
+ export async function injectMessage(cdp, message) {
182
+ // Validate message before processing
183
+ const validation = validateMessage(message);
184
+ if (!validation.valid) {
185
+ return { success: false, error: validation.error };
186
+ }
187
+
188
+ if (!cdp.rootContextId) {
189
+ return { success: false, error: 'No execution context available' };
190
+ }
191
+
192
+ try {
193
+ const result = await cdp.call('Runtime.evaluate', {
194
+ expression: createInjectScript(message),
195
+ contextId: cdp.rootContextId,
196
+ returnByValue: true,
197
+ awaitPromise: true
198
+ });
199
+
200
+ if (result.exceptionDetails) {
201
+ return { success: false, error: result.exceptionDetails.exception?.description || 'Unknown error' };
202
+ }
203
+
204
+ const value = result.result?.value;
205
+ if (value?.ok) {
206
+ console.log(`[Inject] Message sent via ${value.method}`);
207
+ return { success: true, method: value.method };
208
+ }
209
+ return { success: false, error: value?.error || 'Injection failed' };
210
+ } catch (err) {
211
+ console.error('[Inject] CDP call failed:', err.message);
212
+ return { success: false, error: err.message };
213
+ }
214
+ }
@@ -0,0 +1,370 @@
1
+ /**
2
+ * Snapshot capture service - captures DOM snapshots from Kiro via CDP
3
+ */
4
+ import { getLanguageFromExtension } from '../utils/constants.js';
5
+
6
+ /**
7
+ * Capture chat metadata (title, active state)
8
+ * @param {CDPConnection} cdp - CDP connection
9
+ * @returns {Promise<{chatTitle: string, isActive: boolean}>}
10
+ */
11
+ export async function captureMetadata(cdp) {
12
+ if (!cdp.rootContextId) return { chatTitle: '', isActive: false };
13
+
14
+ const script = `(function() {
15
+ let chatTitle = '';
16
+ let isActive = false;
17
+
18
+ const titleSelectors = ['.chat-title', '.conversation-title', '[data-testid="chat-title"]', '.chat-header h1', '.chat-header h2'];
19
+ for (const selector of titleSelectors) {
20
+ const el = document.querySelector(selector);
21
+ if (el && el.textContent) { chatTitle = el.textContent.trim(); break; }
22
+ }
23
+
24
+ const activeIndicators = ['.typing-indicator', '.loading-indicator', '[data-loading="true"]'];
25
+ for (const selector of activeIndicators) {
26
+ if (document.querySelector(selector)) { isActive = true; break; }
27
+ }
28
+ isActive = isActive || document.hasFocus();
29
+
30
+ return { chatTitle, isActive };
31
+ })()`;
32
+
33
+ try {
34
+ const result = await cdp.call('Runtime.evaluate', {
35
+ expression: script,
36
+ contextId: cdp.rootContextId,
37
+ returnByValue: true
38
+ });
39
+ return result.result?.value || { chatTitle: '', isActive: false };
40
+ } catch (err) {
41
+ console.error('[Snapshot] Failed to capture metadata:', err.message);
42
+ return { chatTitle: '', isActive: false };
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Capture CSS styles from the page (run once per connection)
48
+ * @param {CDPConnection} cdp - CDP connection
49
+ * @returns {Promise<string>} - Combined CSS string
50
+ */
51
+ export async function captureCSS(cdp) {
52
+ if (!cdp.rootContextId) return '';
53
+
54
+ const script = `(function() {
55
+ let css = '';
56
+ let targetDoc = document;
57
+ const activeFrame = document.getElementById('active-frame');
58
+ if (activeFrame && activeFrame.contentDocument) targetDoc = activeFrame.contentDocument;
59
+
60
+ const rootStyles = window.getComputedStyle(targetDoc.documentElement);
61
+ const allProps = [];
62
+ for (let i = 0; i < rootStyles.length; i++) allProps.push(rootStyles[i]);
63
+
64
+ // Safely iterate stylesheets with null check
65
+ const styleSheets = targetDoc.styleSheets || [];
66
+ for (const sheet of styleSheets) {
67
+ try {
68
+ if (sheet.cssRules) {
69
+ for (const rule of sheet.cssRules) {
70
+ if (rule.style) {
71
+ for (let i = 0; i < rule.style.length; i++) {
72
+ const prop = rule.style[i];
73
+ if (prop.startsWith('--') && !allProps.includes(prop)) allProps.push(prop);
74
+ }
75
+ }
76
+ }
77
+ }
78
+ } catch (e) {
79
+ // CORS restriction on external stylesheets, skip
80
+ }
81
+ }
82
+
83
+ let cssVars = ':root {\\n';
84
+ for (const prop of allProps) {
85
+ if (prop.startsWith('--')) {
86
+ const value = rootStyles.getPropertyValue(prop).trim();
87
+ if (value) cssVars += ' ' + prop + ': ' + value + ';\\n';
88
+ }
89
+ }
90
+ cssVars += '}\\n\\n';
91
+ css += cssVars;
92
+
93
+ for (const sheet of styleSheets) {
94
+ try {
95
+ if (sheet.cssRules) {
96
+ for (const rule of sheet.cssRules) css += rule.cssText + '\\n';
97
+ }
98
+ } catch (e) {
99
+ // CORS restriction, skip
100
+ }
101
+ }
102
+
103
+ const styleTags = targetDoc.querySelectorAll('style');
104
+ for (const tag of styleTags) css += tag.textContent + '\\n';
105
+
106
+ return css;
107
+ })()`;
108
+
109
+ try {
110
+ const result = await cdp.call('Runtime.evaluate', {
111
+ expression: script,
112
+ contextId: cdp.rootContextId,
113
+ returnByValue: true
114
+ });
115
+ return result.result?.value || '';
116
+ } catch (err) {
117
+ console.error('[Snapshot] Failed to capture CSS:', err.message);
118
+ return '';
119
+ }
120
+ }
121
+
122
+
123
+ /**
124
+ * Capture HTML snapshot of the chat interface
125
+ * @param {CDPConnection} cdp - CDP connection
126
+ * @returns {Promise<{html: string, bodyBg: string, bodyColor: string} | null>}
127
+ */
128
+ export async function captureSnapshot(cdp) {
129
+ if (!cdp.rootContextId) {
130
+ console.log('[Snapshot] No rootContextId available');
131
+ return null;
132
+ }
133
+
134
+ const script = `(function() {
135
+ let targetDoc = document;
136
+ let targetBody = document.body;
137
+
138
+ const activeFrame = document.getElementById('active-frame');
139
+ if (activeFrame && activeFrame.contentDocument) {
140
+ targetDoc = activeFrame.contentDocument;
141
+ targetBody = targetDoc.body;
142
+ }
143
+
144
+ if (!targetBody) return { html: '<div style="padding:20px;color:#888;">No content found</div>', bodyBg: '', bodyColor: '' };
145
+
146
+ const bodyStyles = window.getComputedStyle(targetBody);
147
+ const bodyBg = bodyStyles.backgroundColor || '';
148
+ const bodyColor = bodyStyles.color || '';
149
+
150
+ const scrollContainers = targetDoc.querySelectorAll('[class*="scroll"], [style*="overflow"]');
151
+
152
+ for (const container of scrollContainers) {
153
+ if (container.scrollHeight > container.clientHeight) {
154
+ const isHistoryPanel = container.matches('[class*="history"], [class*="History"], [class*="session-list"], [class*="SessionList"]') ||
155
+ container.closest('[class*="history"], [class*="History"], [class*="session-list"], [class*="SessionList"]');
156
+
157
+ if (isHistoryPanel) {
158
+ container.scrollTop = 0;
159
+ } else {
160
+ container.scrollTop = container.scrollHeight;
161
+ }
162
+ }
163
+ }
164
+
165
+ const clone = targetBody.cloneNode(true);
166
+
167
+ // Capture any portal/overlay content that might be outside the body
168
+ // Radix UI and similar libraries render dropdowns in portals at document root
169
+ const portals = targetDoc.querySelectorAll('[data-radix-portal], [data-radix-popper-content-wrapper], [class*="portal"], [class*="Portal"]');
170
+ portals.forEach(portal => {
171
+ try {
172
+ const portalClone = portal.cloneNode(true);
173
+ clone.appendChild(portalClone);
174
+ } catch(e) {
175
+ // Clone failed, skip
176
+ }
177
+ });
178
+
179
+ // Also check for any floating/overlay elements at document level
180
+ const floatingSelectors = [
181
+ 'body > [role="listbox"]',
182
+ 'body > [role="menu"]',
183
+ 'body > [data-state="open"]',
184
+ 'body > [class*="dropdown"]',
185
+ 'body > div[style*="position: absolute"]',
186
+ 'body > div[style*="position: fixed"]'
187
+ ];
188
+
189
+ floatingSelectors.forEach(sel => {
190
+ try {
191
+ targetDoc.querySelectorAll(sel).forEach(el => {
192
+ const elClone = el.cloneNode(true);
193
+ clone.appendChild(elClone);
194
+ });
195
+ } catch(e) {
196
+ // Selector failed, skip
197
+ }
198
+ });
199
+
200
+ // Remove ONLY tooltips from clone - preserve everything else
201
+ try {
202
+ clone.querySelectorAll('[role="tooltip"]').forEach(el => el.remove());
203
+ } catch(e) {
204
+ // Removal failed, continue
205
+ }
206
+
207
+ // Fix SVG currentColor
208
+ clone.querySelectorAll('svg').forEach(svg => {
209
+ try {
210
+ const computedColor = '#cccccc';
211
+ svg.querySelectorAll('[fill="currentColor"]').forEach(el => el.setAttribute('fill', computedColor));
212
+ svg.querySelectorAll('[stroke="currentColor"]').forEach(el => el.setAttribute('stroke', computedColor));
213
+ } catch(e) {
214
+ // SVG fix failed, continue
215
+ }
216
+ });
217
+
218
+ // Remove placeholder text
219
+ try {
220
+ clone.querySelectorAll('[contenteditable="true"], [data-lexical-editor="true"]').forEach(editable => {
221
+ const parent = editable.parentElement;
222
+ if (parent) {
223
+ Array.from(parent.children).forEach(sibling => {
224
+ if (sibling === editable) return;
225
+ const text = (sibling.textContent || '').toLowerCase();
226
+ if (text.includes('ask') || text.includes('question') || text.includes('task') || text.includes('describe')) {
227
+ sibling.remove();
228
+ }
229
+ });
230
+ }
231
+ });
232
+
233
+ clone.querySelectorAll('[class*="placeholder"], [class*="Placeholder"], [data-placeholder]').forEach(el => {
234
+ if (!el.matches('[contenteditable], [data-lexical-editor], textarea, input')) {
235
+ if (!el.querySelector('[contenteditable], [data-lexical-editor], textarea, input')) el.remove();
236
+ }
237
+ });
238
+ } catch(e) {
239
+ // Placeholder removal failed, continue
240
+ }
241
+
242
+ return { html: clone.outerHTML, bodyBg, bodyColor };
243
+ })()`;
244
+
245
+ try {
246
+ const result = await cdp.call('Runtime.evaluate', {
247
+ expression: script,
248
+ contextId: cdp.rootContextId,
249
+ returnByValue: true
250
+ });
251
+
252
+ return result.result?.value || null;
253
+ } catch (err) {
254
+ console.error('[Snapshot] Failed to capture HTML:', err.message);
255
+ return null;
256
+ }
257
+ }
258
+
259
+ /**
260
+ * Capture Editor panel snapshot (currently open file)
261
+ * @param {CDPConnection} cdp - CDP connection
262
+ * @returns {Promise<{fileName: string, language: string, content: string, lineCount: number, hasContent: boolean} | null>}
263
+ */
264
+ export async function captureEditor(cdp) {
265
+ if (!cdp.rootContextId) return null;
266
+
267
+ const script = `(function() {
268
+ let targetDoc = document;
269
+ const activeFrame = document.getElementById('active-frame');
270
+ if (activeFrame && activeFrame.contentDocument) targetDoc = activeFrame.contentDocument;
271
+
272
+ const result = { html: '', fileName: '', language: '', content: '', lineCount: 0, hasContent: false };
273
+
274
+ // Get active tab / file name
275
+ const tabSelectors = ['.tab.active .label-name', '.tab.active', '[role="tab"][aria-selected="true"]'];
276
+ for (const selector of tabSelectors) {
277
+ try {
278
+ const tab = targetDoc.querySelector(selector);
279
+ if (tab && tab.textContent) {
280
+ result.fileName = tab.textContent.trim().split('\\n')[0].trim();
281
+ if (result.fileName) break;
282
+ }
283
+ } catch(e) {
284
+ // Selector failed, continue
285
+ }
286
+ }
287
+
288
+ // Try Monaco API
289
+ try {
290
+ const monacoEditors = targetDoc.querySelectorAll('.monaco-editor');
291
+ for (const editorEl of monacoEditors) {
292
+ const editorInstance = editorEl.__vscode_editor__ || editorEl._editor ||
293
+ (window.monaco && window.monaco.editor && window.monaco.editor.getEditors && window.monaco.editor.getEditors()[0]);
294
+ if (editorInstance && editorInstance.getModel) {
295
+ const model = editorInstance.getModel();
296
+ if (model) {
297
+ result.content = model.getValue();
298
+ result.lineCount = model.getLineCount();
299
+ result.language = model.getLanguageId ? model.getLanguageId() : '';
300
+ result.hasContent = true;
301
+ break;
302
+ }
303
+ }
304
+ }
305
+ } catch(e) {
306
+ // Monaco API not available, continue
307
+ }
308
+
309
+ // Fallback: Extract from view-lines
310
+ if (!result.content) {
311
+ const viewLines = targetDoc.querySelector('.monaco-editor .view-lines');
312
+ if (viewLines) {
313
+ const lines = viewLines.querySelectorAll('.view-line');
314
+ if (lines.length > 0) {
315
+ let codeContent = '';
316
+ let minLineNum = Infinity, maxLineNum = 0;
317
+ const lineMap = new Map();
318
+
319
+ lines.forEach(line => {
320
+ const top = parseFloat(line.style.top) || 0;
321
+ const lineNum = Math.round(top / 19) + 1;
322
+ lineMap.set(lineNum, line.textContent || '');
323
+ minLineNum = Math.min(minLineNum, lineNum);
324
+ maxLineNum = Math.max(maxLineNum, lineNum);
325
+ });
326
+
327
+ for (let i = minLineNum; i <= Math.min(maxLineNum, minLineNum + 500); i++) {
328
+ codeContent += (lineMap.get(i) || '') + '\\n';
329
+ }
330
+
331
+ result.content = codeContent;
332
+ result.lineCount = maxLineNum;
333
+ result.startLine = minLineNum;
334
+ result.hasContent = codeContent.trim().length > 0;
335
+ if (minLineNum > 1) {
336
+ result.isPartial = true;
337
+ result.note = 'Showing lines ' + minLineNum + '-' + maxLineNum + '. Scroll in Kiro to see other parts.';
338
+ }
339
+ }
340
+ }
341
+ }
342
+
343
+ // Detect language from filename
344
+ if (!result.language && result.fileName) {
345
+ const ext = result.fileName.split('.').pop()?.toLowerCase();
346
+ const extMap = {
347
+ 'ts': 'typescript', 'tsx': 'typescript', 'js': 'javascript', 'jsx': 'javascript',
348
+ 'py': 'python', 'java': 'java', 'html': 'html', 'css': 'css', 'json': 'json',
349
+ 'md': 'markdown', 'yaml': 'yaml', 'yml': 'yaml', 'go': 'go', 'rs': 'rust',
350
+ 'c': 'c', 'cpp': 'cpp', 'h': 'c', 'cs': 'csharp', 'rb': 'ruby', 'php': 'php',
351
+ 'sql': 'sql', 'sh': 'bash', 'vue': 'vue', 'svelte': 'svelte'
352
+ };
353
+ result.language = extMap[ext] || ext || '';
354
+ }
355
+
356
+ return result;
357
+ })()`;
358
+
359
+ try {
360
+ const result = await cdp.call('Runtime.evaluate', {
361
+ expression: script,
362
+ contextId: cdp.rootContextId,
363
+ returnByValue: true
364
+ });
365
+ return result.result?.value || null;
366
+ } catch (err) {
367
+ console.error('[Editor] Failed to capture:', err.message);
368
+ return null;
369
+ }
370
+ }
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Shared constants used across the application
3
+ * Centralizes configuration to eliminate duplication and improve maintainability
4
+ */
5
+
6
+ /**
7
+ * CDP ports to scan for Kiro instances
8
+ * @type {number[]}
9
+ */
10
+ export const CDP_PORTS = [9000, 9001, 9002, 9003, 9222, 9229];
11
+
12
+ /**
13
+ * Model names for AI model detection and matching
14
+ * Order matters: check specific names (opus, sonnet, haiku) BEFORE generic (claude)
15
+ * @type {string[]}
16
+ */
17
+ export const MODEL_NAMES = ['auto', 'opus', 'sonnet', 'haiku', 'gpt', 'claude', 'gemini', 'llama'];
18
+
19
+ /**
20
+ * Code file extensions for workspace file filtering
21
+ * @type {Set<string>}
22
+ */
23
+ export const CODE_EXTENSIONS = new Set([
24
+ '.ts', '.tsx', '.js', '.jsx', '.py', '.java', '.go', '.rs',
25
+ '.html', '.css', '.scss', '.json', '.yaml', '.yml', '.md',
26
+ '.sql', '.sh', '.c', '.cpp', '.h', '.cs', '.vue', '.svelte', '.rb', '.php'
27
+ ]);
28
+
29
+ /**
30
+ * File extension to language mapping for syntax highlighting
31
+ * @type {Object<string, string>}
32
+ */
33
+ export const EXTENSION_TO_LANGUAGE = {
34
+ '.ts': 'typescript',
35
+ '.tsx': 'typescript',
36
+ '.js': 'javascript',
37
+ '.jsx': 'javascript',
38
+ '.py': 'python',
39
+ '.html': 'html',
40
+ '.css': 'css',
41
+ '.scss': 'scss',
42
+ '.json': 'json',
43
+ '.md': 'markdown',
44
+ '.yaml': 'yaml',
45
+ '.yml': 'yaml',
46
+ '.go': 'go',
47
+ '.rs': 'rust',
48
+ '.java': 'java',
49
+ '.c': 'c',
50
+ '.cpp': 'cpp',
51
+ '.h': 'c',
52
+ '.cs': 'csharp',
53
+ '.rb': 'ruby',
54
+ '.php': 'php',
55
+ '.sql': 'sql',
56
+ '.sh': 'bash',
57
+ '.vue': 'vue',
58
+ '.svelte': 'svelte'
59
+ };
60
+
61
+ /**
62
+ * Get language from file extension
63
+ * @param {string} filename - File name or path
64
+ * @returns {string} - Language identifier or extension without dot
65
+ */
66
+ export function getLanguageFromExtension(filename) {
67
+ const ext = filename.includes('.') ? '.' + filename.split('.').pop().toLowerCase() : '';
68
+ return EXTENSION_TO_LANGUAGE[ext] || ext.slice(1) || 'text';
69
+ }
70
+
71
+ /**
72
+ * Check if a file has a code extension
73
+ * @param {string} filename - File name or path
74
+ * @returns {boolean}
75
+ */
76
+ export function isCodeFile(filename) {
77
+ const ext = filename.includes('.') ? '.' + filename.split('.').pop().toLowerCase() : '';
78
+ return CODE_EXTENSIONS.has(ext);
79
+ }
80
+
81
+ /**
82
+ * CDP call timeout in milliseconds
83
+ * @type {number}
84
+ */
85
+ export const CDP_CALL_TIMEOUT = 10000;
86
+
87
+ /**
88
+ * HTTP request timeout in milliseconds
89
+ * @type {number}
90
+ */
91
+ export const HTTP_TIMEOUT = 2000;
92
+
93
+ /**
94
+ * Discovery polling intervals
95
+ */
96
+ export const DISCOVERY_INTERVAL_ACTIVE = 10000; // 10 seconds when changes detected
97
+ export const DISCOVERY_INTERVAL_STABLE = 30000; // 30 seconds when stable
98
+
99
+ /**
100
+ * Snapshot polling intervals
101
+ */
102
+ export const SNAPSHOT_INTERVAL_ACTIVE = 1000; // 1 second when active
103
+ export const SNAPSHOT_INTERVAL_IDLE = 3000; // 3 seconds when idle
104
+ export const SNAPSHOT_IDLE_THRESHOLD = 10000; // 10 seconds before considered idle
105
+
106
+ /**
107
+ * Maximum depth for recursive file search
108
+ * @type {number}
109
+ */
110
+ export const MAX_FILE_SEARCH_DEPTH = 4;
111
+
112
+ /**
113
+ * Maximum depth for workspace file collection
114
+ * @type {number}
115
+ */
116
+ export const MAX_WORKSPACE_DEPTH = 5;