kiro-mobile-bridge 1.0.7 → 1.0.8

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,331 @@
1
+ /**
2
+ * Snapshot capture service - captures DOM snapshots from Kiro via CDP
3
+ */
4
+
5
+ /**
6
+ * Capture chat metadata (title, active state)
7
+ * @param {CDPConnection} cdp - CDP connection
8
+ * @returns {Promise<{chatTitle: string, isActive: boolean}>}
9
+ */
10
+ export async function captureMetadata(cdp) {
11
+ if (!cdp.rootContextId) return { chatTitle: '', isActive: false };
12
+
13
+ const script = `(function() {
14
+ let chatTitle = '';
15
+ let isActive = false;
16
+
17
+ const titleSelectors = ['.chat-title', '.conversation-title', '[data-testid="chat-title"]', '.chat-header h1', '.chat-header h2'];
18
+ for (const selector of titleSelectors) {
19
+ const el = document.querySelector(selector);
20
+ if (el && el.textContent) { chatTitle = el.textContent.trim(); break; }
21
+ }
22
+
23
+ const activeIndicators = ['.typing-indicator', '.loading-indicator', '[data-loading="true"]'];
24
+ for (const selector of activeIndicators) {
25
+ if (document.querySelector(selector)) { isActive = true; break; }
26
+ }
27
+ isActive = isActive || document.hasFocus();
28
+
29
+ return { chatTitle, isActive };
30
+ })()`;
31
+
32
+ try {
33
+ const result = await cdp.call('Runtime.evaluate', {
34
+ expression: script,
35
+ contextId: cdp.rootContextId,
36
+ returnByValue: true
37
+ });
38
+ return result.result?.value || { chatTitle: '', isActive: false };
39
+ } catch (err) {
40
+ console.error('[Snapshot] Failed to capture metadata:', err.message);
41
+ return { chatTitle: '', isActive: false };
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Capture CSS styles from the page (run once per connection)
47
+ * @param {CDPConnection} cdp - CDP connection
48
+ * @returns {Promise<string>} - Combined CSS string
49
+ */
50
+ export async function captureCSS(cdp) {
51
+ if (!cdp.rootContextId) return '';
52
+
53
+ const script = `(function() {
54
+ let css = '';
55
+ let targetDoc = document;
56
+ const activeFrame = document.getElementById('active-frame');
57
+ if (activeFrame && activeFrame.contentDocument) targetDoc = activeFrame.contentDocument;
58
+
59
+ const rootStyles = window.getComputedStyle(targetDoc.documentElement);
60
+ const allProps = [];
61
+ for (let i = 0; i < rootStyles.length; i++) allProps.push(rootStyles[i]);
62
+
63
+ for (const sheet of targetDoc.styleSheets) {
64
+ try {
65
+ if (sheet.cssRules) {
66
+ for (const rule of sheet.cssRules) {
67
+ if (rule.style) {
68
+ for (let i = 0; i < rule.style.length; i++) {
69
+ const prop = rule.style[i];
70
+ if (prop.startsWith('--') && !allProps.includes(prop)) allProps.push(prop);
71
+ }
72
+ }
73
+ }
74
+ }
75
+ } catch (e) {}
76
+ }
77
+
78
+ let cssVars = ':root {\\n';
79
+ for (const prop of allProps) {
80
+ if (prop.startsWith('--')) {
81
+ const value = rootStyles.getPropertyValue(prop).trim();
82
+ if (value) cssVars += ' ' + prop + ': ' + value + ';\\n';
83
+ }
84
+ }
85
+ cssVars += '}\\n\\n';
86
+ css += cssVars;
87
+
88
+ for (const sheet of targetDoc.styleSheets) {
89
+ try {
90
+ if (sheet.cssRules) {
91
+ for (const rule of sheet.cssRules) css += rule.cssText + '\\n';
92
+ }
93
+ } catch (e) {}
94
+ }
95
+
96
+ const styleTags = targetDoc.querySelectorAll('style');
97
+ for (const tag of styleTags) css += tag.textContent + '\\n';
98
+
99
+ return css;
100
+ })()`;
101
+
102
+ try {
103
+ const result = await cdp.call('Runtime.evaluate', {
104
+ expression: script,
105
+ contextId: cdp.rootContextId,
106
+ returnByValue: true
107
+ });
108
+ return result.result?.value || '';
109
+ } catch (err) {
110
+ console.error('[Snapshot] Failed to capture CSS:', err.message);
111
+ return '';
112
+ }
113
+ }
114
+
115
+
116
+ /**
117
+ * Capture HTML snapshot of the chat interface
118
+ * @param {CDPConnection} cdp - CDP connection
119
+ * @returns {Promise<{html: string, bodyBg: string, bodyColor: string} | null>}
120
+ */
121
+ export async function captureSnapshot(cdp) {
122
+ if (!cdp.rootContextId) {
123
+ console.log('[Snapshot] No rootContextId available');
124
+ return null;
125
+ }
126
+
127
+ const script = `(function() {
128
+ let targetDoc = document;
129
+ let targetBody = document.body;
130
+
131
+ const activeFrame = document.getElementById('active-frame');
132
+ if (activeFrame && activeFrame.contentDocument) {
133
+ targetDoc = activeFrame.contentDocument;
134
+ targetBody = targetDoc.body;
135
+ }
136
+
137
+ if (!targetBody) return { html: '<div style="padding:20px;color:#888;">No content found</div>', bodyBg: '', bodyColor: '' };
138
+
139
+ const bodyStyles = window.getComputedStyle(targetBody);
140
+ const bodyBg = bodyStyles.backgroundColor || '';
141
+ const bodyColor = bodyStyles.color || '';
142
+
143
+ const scrollContainers = targetDoc.querySelectorAll('[class*="scroll"], [style*="overflow"]');
144
+
145
+ for (const container of scrollContainers) {
146
+ if (container.scrollHeight > container.clientHeight) {
147
+ // Only check class names for history detection - NOT date patterns
148
+ const isHistoryPanel = container.matches('[class*="history"], [class*="History"], [class*="session-list"], [class*="SessionList"]') ||
149
+ container.closest('[class*="history"], [class*="History"], [class*="session-list"], [class*="SessionList"]');
150
+
151
+ if (isHistoryPanel) {
152
+ container.scrollTop = 0; // Scroll to TOP for history panels
153
+ } else {
154
+ container.scrollTop = container.scrollHeight; // Scroll to BOTTOM for chat messages
155
+ }
156
+ }
157
+ }
158
+
159
+ const clone = targetBody.cloneNode(true);
160
+
161
+ // Remove tooltips, popovers, overlays from clone
162
+ const elementsToRemove = [
163
+ '[role="tooltip"]', '[data-tooltip]', '[class*="tooltip"]:not(button)', '[class*="Tooltip"]:not(button)',
164
+ '[class*="popover"]:not(button)', '[class*="Popover"]:not(button)', '[class*="dropdown-menu"]',
165
+ '[class*="dropdownMenu"]', '[class*="modal"]', '[class*="Modal"]',
166
+ '[style*="position: fixed"]:not(button):not([class*="input"]):not([class*="chat"])'
167
+ ];
168
+
169
+ elementsToRemove.forEach(selector => {
170
+ try {
171
+ clone.querySelectorAll(selector).forEach(el => {
172
+ const isTooltip = el.matches('[role="tooltip"], [class*="tooltip"], [class*="Tooltip"]');
173
+ const isImportantUI = el.matches('[class*="model"], [class*="context"], [class*="input"], button, [role="button"]');
174
+ if (isTooltip || !isImportantUI) el.remove();
175
+ });
176
+ } catch(e) {}
177
+ });
178
+
179
+ // Fix SVG currentColor
180
+ clone.querySelectorAll('svg').forEach(svg => {
181
+ try {
182
+ const computedColor = '#cccccc';
183
+ svg.querySelectorAll('[fill="currentColor"]').forEach(el => el.setAttribute('fill', computedColor));
184
+ svg.querySelectorAll('[stroke="currentColor"]').forEach(el => el.setAttribute('stroke', computedColor));
185
+ } catch(e) {}
186
+ });
187
+
188
+ // Remove placeholder text
189
+ try {
190
+ clone.querySelectorAll('[contenteditable="true"], [data-lexical-editor="true"]').forEach(editable => {
191
+ const parent = editable.parentElement;
192
+ if (parent) {
193
+ Array.from(parent.children).forEach(sibling => {
194
+ if (sibling === editable) return;
195
+ const text = (sibling.textContent || '').toLowerCase();
196
+ if (text.includes('ask') || text.includes('question') || text.includes('task') || text.includes('describe')) {
197
+ sibling.remove();
198
+ }
199
+ });
200
+ }
201
+ });
202
+
203
+ clone.querySelectorAll('[class*="placeholder"], [class*="Placeholder"], [data-placeholder]').forEach(el => {
204
+ if (!el.matches('[contenteditable], [data-lexical-editor], textarea, input')) {
205
+ if (!el.querySelector('[contenteditable], [data-lexical-editor], textarea, input')) el.remove();
206
+ }
207
+ });
208
+ } catch(e) {}
209
+
210
+ return { html: clone.outerHTML, bodyBg, bodyColor };
211
+ })()`;
212
+
213
+ try {
214
+ const result = await cdp.call('Runtime.evaluate', {
215
+ expression: script,
216
+ contextId: cdp.rootContextId,
217
+ returnByValue: true
218
+ });
219
+ return result.result?.value || null;
220
+ } catch (err) {
221
+ console.error('[Snapshot] Failed to capture HTML:', err.message);
222
+ return null;
223
+ }
224
+ }
225
+
226
+ /**
227
+ * Capture Editor panel snapshot (currently open file)
228
+ * @param {CDPConnection} cdp - CDP connection
229
+ * @returns {Promise<{fileName: string, language: string, content: string, lineCount: number, hasContent: boolean} | null>}
230
+ */
231
+ export async function captureEditor(cdp) {
232
+ if (!cdp.rootContextId) return null;
233
+
234
+ const script = `(function() {
235
+ let targetDoc = document;
236
+ const activeFrame = document.getElementById('active-frame');
237
+ if (activeFrame && activeFrame.contentDocument) targetDoc = activeFrame.contentDocument;
238
+
239
+ const result = { html: '', fileName: '', language: '', content: '', lineCount: 0, hasContent: false };
240
+
241
+ // Get active tab / file name
242
+ const tabSelectors = ['.tab.active .label-name', '.tab.active', '[role="tab"][aria-selected="true"]'];
243
+ for (const selector of tabSelectors) {
244
+ try {
245
+ const tab = targetDoc.querySelector(selector);
246
+ if (tab && tab.textContent) {
247
+ result.fileName = tab.textContent.trim().split('\\n')[0].trim();
248
+ if (result.fileName) break;
249
+ }
250
+ } catch(e) {}
251
+ }
252
+
253
+ // Try Monaco API
254
+ try {
255
+ const monacoEditors = targetDoc.querySelectorAll('.monaco-editor');
256
+ for (const editorEl of monacoEditors) {
257
+ const editorInstance = editorEl.__vscode_editor__ || editorEl._editor ||
258
+ (window.monaco && window.monaco.editor.getEditors && window.monaco.editor.getEditors()[0]);
259
+ if (editorInstance && editorInstance.getModel) {
260
+ const model = editorInstance.getModel();
261
+ if (model) {
262
+ result.content = model.getValue();
263
+ result.lineCount = model.getLineCount();
264
+ result.language = model.getLanguageId ? model.getLanguageId() : '';
265
+ result.hasContent = true;
266
+ break;
267
+ }
268
+ }
269
+ }
270
+ } catch(e) {}
271
+
272
+ // Fallback: Extract from view-lines
273
+ if (!result.content) {
274
+ const viewLines = targetDoc.querySelector('.monaco-editor .view-lines');
275
+ if (viewLines) {
276
+ const lines = viewLines.querySelectorAll('.view-line');
277
+ if (lines.length > 0) {
278
+ let codeContent = '';
279
+ let minLineNum = Infinity, maxLineNum = 0;
280
+ const lineMap = new Map();
281
+
282
+ lines.forEach(line => {
283
+ const top = parseFloat(line.style.top) || 0;
284
+ const lineNum = Math.round(top / 19) + 1;
285
+ lineMap.set(lineNum, line.textContent || '');
286
+ minLineNum = Math.min(minLineNum, lineNum);
287
+ maxLineNum = Math.max(maxLineNum, lineNum);
288
+ });
289
+
290
+ for (let i = minLineNum; i <= Math.min(maxLineNum, minLineNum + 500); i++) {
291
+ codeContent += (lineMap.get(i) || '') + '\\n';
292
+ }
293
+
294
+ result.content = codeContent;
295
+ result.lineCount = maxLineNum;
296
+ result.startLine = minLineNum;
297
+ result.hasContent = codeContent.trim().length > 0;
298
+ if (minLineNum > 1) {
299
+ result.isPartial = true;
300
+ result.note = 'Showing lines ' + minLineNum + '-' + maxLineNum + '. Scroll in Kiro to see other parts.';
301
+ }
302
+ }
303
+ }
304
+ }
305
+
306
+ // Detect language from filename
307
+ if (!result.language && result.fileName) {
308
+ const ext = result.fileName.split('.').pop()?.toLowerCase();
309
+ const extMap = {
310
+ 'ts': 'typescript', 'tsx': 'typescript', 'js': 'javascript', 'jsx': 'javascript',
311
+ 'py': 'python', 'java': 'java', 'html': 'html', 'css': 'css', 'json': 'json',
312
+ 'md': 'markdown', 'yaml': 'yaml', 'yml': 'yaml', 'go': 'go', 'rs': 'rust'
313
+ };
314
+ result.language = extMap[ext] || ext || '';
315
+ }
316
+
317
+ return result;
318
+ })()`;
319
+
320
+ try {
321
+ const result = await cdp.call('Runtime.evaluate', {
322
+ expression: script,
323
+ contextId: cdp.rootContextId,
324
+ returnByValue: true
325
+ });
326
+ return result.result?.value || null;
327
+ } catch (err) {
328
+ console.error('[Editor] Failed to capture:', err.message);
329
+ return null;
330
+ }
331
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Hash utilities for content change detection
3
+ */
4
+ import crypto from 'crypto';
5
+
6
+ /**
7
+ * Generate a unique ID from a string (e.g., WebSocket URL)
8
+ * @param {string} input - String to hash
9
+ * @returns {string} - 8-character hash ID
10
+ */
11
+ export function generateId(input) {
12
+ return crypto.createHash('md5').update(input).digest('hex').substring(0, 8);
13
+ }
14
+
15
+ /**
16
+ * Compute MD5 hash for change detection
17
+ * @param {string} content - Content to hash
18
+ * @returns {string} - Full MD5 hash
19
+ */
20
+ export function computeHash(content) {
21
+ return crypto.createHash('md5').update(content).digest('hex');
22
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Network utilities
3
+ */
4
+ import { networkInterfaces } from 'os';
5
+
6
+ /**
7
+ * Get local IP address for LAN access
8
+ * @returns {string} - Local IP or 'localhost'
9
+ */
10
+ export function getLocalIP() {
11
+ const interfaces = networkInterfaces();
12
+ for (const name of Object.keys(interfaces)) {
13
+ for (const iface of interfaces[name]) {
14
+ if (iface.family === 'IPv4' && !iface.internal) {
15
+ return iface.address;
16
+ }
17
+ }
18
+ }
19
+ return 'localhost';
20
+ }