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.
- package/README.md +9 -21
- package/package.json +1 -1
- package/src/public/index.html +1162 -1623
- package/src/routes/api.js +358 -0
- package/src/server.js +253 -2575
- package/src/services/cdp.js +156 -0
- package/src/services/click.js +282 -0
- package/src/services/message.js +206 -0
- package/src/services/snapshot.js +331 -0
- package/src/utils/hash.js +22 -0
- package/src/utils/network.js +20 -0
|
@@ -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
|
+
}
|