kiro-mobile-bridge 1.0.8 → 1.0.11
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 -5
- package/package.json +1 -1
- package/src/public/index.html +258 -11
- package/src/routes/api.js +257 -76
- package/src/server.js +33 -17
- package/src/services/cdp.js +70 -16
- package/src/services/click.js +325 -74
- package/src/services/message.js +9 -1
- package/src/services/snapshot.js +64 -25
- package/src/utils/constants.js +116 -0
- package/src/utils/hash.js +12 -0
- package/src/utils/network.js +72 -2
- package/src/utils/security.js +160 -0
package/src/routes/api.js
CHANGED
|
@@ -4,8 +4,23 @@
|
|
|
4
4
|
import { Router } from 'express';
|
|
5
5
|
import fs from 'fs/promises';
|
|
6
6
|
import path from 'path';
|
|
7
|
+
import os from 'os';
|
|
7
8
|
import { injectMessage } from '../services/message.js';
|
|
8
9
|
import { clickElement } from '../services/click.js';
|
|
10
|
+
import {
|
|
11
|
+
validatePathWithinRoot,
|
|
12
|
+
sanitizeClickInfo,
|
|
13
|
+
validateMessage,
|
|
14
|
+
sanitizeFilePath
|
|
15
|
+
} from '../utils/security.js';
|
|
16
|
+
import {
|
|
17
|
+
CODE_EXTENSIONS,
|
|
18
|
+
EXTENSION_TO_LANGUAGE,
|
|
19
|
+
getLanguageFromExtension,
|
|
20
|
+
isCodeFile,
|
|
21
|
+
MAX_FILE_SEARCH_DEPTH,
|
|
22
|
+
MAX_WORKSPACE_DEPTH
|
|
23
|
+
} from '../utils/constants.js';
|
|
9
24
|
|
|
10
25
|
/**
|
|
11
26
|
* Create API router
|
|
@@ -46,13 +61,17 @@ export function createApiRouter(cascades, mainWindowCDP) {
|
|
|
46
61
|
if (!cascade) return res.status(404).json({ error: 'Cascade not found' });
|
|
47
62
|
|
|
48
63
|
const { message } = req.body;
|
|
49
|
-
|
|
50
|
-
|
|
64
|
+
|
|
65
|
+
// Validate message input
|
|
66
|
+
const validation = validateMessage(message);
|
|
67
|
+
if (!validation.valid) {
|
|
68
|
+
return res.status(400).json({ error: validation.error });
|
|
51
69
|
}
|
|
52
70
|
|
|
53
71
|
if (!cascade.cdp) return res.status(503).json({ error: 'CDP connection not available' });
|
|
54
72
|
|
|
55
|
-
|
|
73
|
+
// Log message length only, not content (security)
|
|
74
|
+
console.log(`[Send] Message to ${req.params.id}: ${message.length} chars`);
|
|
56
75
|
const result = await injectMessage(cascade.cdp, message);
|
|
57
76
|
|
|
58
77
|
if (result.success) {
|
|
@@ -68,81 +87,185 @@ export function createApiRouter(cascades, mainWindowCDP) {
|
|
|
68
87
|
if (!cascade) return res.status(404).json({ error: 'Cascade not found' });
|
|
69
88
|
if (!cascade.cdp?.rootContextId) return res.status(503).json({ error: 'CDP not available' });
|
|
70
89
|
|
|
71
|
-
|
|
72
|
-
|
|
90
|
+
// Validate and sanitize click info
|
|
91
|
+
const { valid, sanitized, error } = sanitizeClickInfo(req.body);
|
|
92
|
+
if (!valid) {
|
|
93
|
+
return res.status(400).json({ error: error || 'Invalid click info' });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
console.log(`[Click] ${sanitized.text?.substring(0, 30) || sanitized.ariaLabel || sanitized.tag || 'element'}`);
|
|
73
97
|
|
|
74
98
|
try {
|
|
75
|
-
const result = await clickElement(cascade.cdp,
|
|
99
|
+
const result = await clickElement(cascade.cdp, sanitized);
|
|
76
100
|
res.json(result);
|
|
77
101
|
} catch (err) {
|
|
102
|
+
console.error('[Click] Error:', err.message);
|
|
78
103
|
res.status(500).json({ success: false, error: err.message });
|
|
79
104
|
}
|
|
80
105
|
});
|
|
81
106
|
|
|
107
|
+
// GET /debug-model/:id - Debug model selector structure
|
|
108
|
+
router.get('/debug-model/:id', async (req, res) => {
|
|
109
|
+
const cascade = cascades.get(req.params.id);
|
|
110
|
+
if (!cascade) return res.status(404).json({ error: 'Cascade not found' });
|
|
111
|
+
if (!cascade.cdp?.rootContextId) return res.status(503).json({ error: 'CDP not available' });
|
|
112
|
+
|
|
113
|
+
const script = `(function() {
|
|
114
|
+
let targetDoc = document;
|
|
115
|
+
const activeFrame = document.getElementById('active-frame');
|
|
116
|
+
if (activeFrame && activeFrame.contentDocument) targetDoc = activeFrame.contentDocument;
|
|
117
|
+
|
|
118
|
+
const results = {
|
|
119
|
+
dropdownOpen: false,
|
|
120
|
+
dropdownItems: [],
|
|
121
|
+
dropdownContainer: null,
|
|
122
|
+
autopilotToggle: null
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// Check if dropdown is open
|
|
126
|
+
const openDropdown = targetDoc.querySelector('.kiro-dropdown-menu, [class*="dropdown-menu"], [class*="dropdown-content"], [role="listbox"], [role="menu"]');
|
|
127
|
+
if (openDropdown && openDropdown.offsetParent !== null) {
|
|
128
|
+
results.dropdownOpen = true;
|
|
129
|
+
results.dropdownContainer = {
|
|
130
|
+
tag: openDropdown.tagName,
|
|
131
|
+
className: (openDropdown.className || '').substring(0, 100),
|
|
132
|
+
role: openDropdown.getAttribute('role'),
|
|
133
|
+
innerHTML: openDropdown.innerHTML.substring(0, 1000)
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
// Find all items in the dropdown
|
|
137
|
+
const items = openDropdown.querySelectorAll('.kiro-dropdown-item, [role="option"], [role="menuitem"], [class*="dropdown-item"], [class*="menu-item"], > div, > button');
|
|
138
|
+
items.forEach(item => {
|
|
139
|
+
results.dropdownItems.push({
|
|
140
|
+
tag: item.tagName,
|
|
141
|
+
className: (item.className || '').substring(0, 80),
|
|
142
|
+
text: (item.textContent || '').substring(0, 100),
|
|
143
|
+
role: item.getAttribute('role'),
|
|
144
|
+
dataValue: item.getAttribute('data-value'),
|
|
145
|
+
onclick: !!item.onclick,
|
|
146
|
+
cursor: window.getComputedStyle(item).cursor
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Find Autopilot toggle
|
|
152
|
+
const autopilotElements = targetDoc.querySelectorAll('[class*="toggle"], input[type="checkbox"]');
|
|
153
|
+
for (const el of autopilotElements) {
|
|
154
|
+
const parent = el.closest('.kiro-toggle-switch, [class*="toggle"]');
|
|
155
|
+
if (parent) {
|
|
156
|
+
const label = parent.querySelector('label');
|
|
157
|
+
const labelText = label ? label.textContent.toLowerCase() : '';
|
|
158
|
+
if (labelText.includes('autopilot') || labelText.includes('auto')) {
|
|
159
|
+
results.autopilotToggle = {
|
|
160
|
+
toggleElement: {
|
|
161
|
+
tag: el.tagName,
|
|
162
|
+
type: el.type,
|
|
163
|
+
className: (el.className || '').substring(0, 80),
|
|
164
|
+
checked: el.checked,
|
|
165
|
+
id: el.id
|
|
166
|
+
},
|
|
167
|
+
parentElement: {
|
|
168
|
+
tag: parent.tagName,
|
|
169
|
+
className: (parent.className || '').substring(0, 80),
|
|
170
|
+
onclick: !!parent.onclick
|
|
171
|
+
},
|
|
172
|
+
labelText: labelText
|
|
173
|
+
};
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return results;
|
|
180
|
+
})()`;
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
const result = await cascade.cdp.call('Runtime.evaluate', {
|
|
184
|
+
expression: script,
|
|
185
|
+
contextId: cascade.cdp.rootContextId,
|
|
186
|
+
returnByValue: true
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
const data = result.result?.value;
|
|
190
|
+
console.log('[Debug] UI structure:', JSON.stringify(data, null, 2));
|
|
191
|
+
res.json(data);
|
|
192
|
+
} catch (err) {
|
|
193
|
+
res.status(500).json({ error: err.message });
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
|
|
82
197
|
// POST /readFile/:id - Read file from filesystem
|
|
83
198
|
router.post('/readFile/:id', async (req, res) => {
|
|
84
199
|
const cascade = cascades.get(req.params.id);
|
|
85
200
|
if (!cascade) return res.status(404).json({ error: 'Cascade not found' });
|
|
86
201
|
|
|
87
202
|
const { filePath } = req.body;
|
|
88
|
-
if (!filePath
|
|
203
|
+
if (!filePath || typeof filePath !== 'string') {
|
|
204
|
+
return res.status(400).json({ error: 'filePath is required and must be a string' });
|
|
205
|
+
}
|
|
89
206
|
|
|
90
|
-
|
|
207
|
+
// Sanitize the file path
|
|
208
|
+
const sanitizedPath = sanitizeFilePath(filePath);
|
|
209
|
+
if (!sanitizedPath) {
|
|
210
|
+
return res.status(400).json({ error: 'Invalid file path' });
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
console.log(`[ReadFile] Request for: ${sanitizedPath}`);
|
|
91
214
|
|
|
92
215
|
try {
|
|
93
|
-
|
|
94
|
-
const
|
|
95
|
-
filePath,
|
|
96
|
-
path.join(process.cwd(), filePath),
|
|
97
|
-
path.join(process.cwd(), 'src', filePath),
|
|
98
|
-
path.join(process.cwd(), 'public', filePath)
|
|
99
|
-
];
|
|
216
|
+
// Get workspace root
|
|
217
|
+
const workspaceRoot = await getWorkspaceRoot(mainWindowCDP) || process.cwd();
|
|
100
218
|
|
|
101
|
-
//
|
|
102
|
-
|
|
103
|
-
const workspaceRoot = await getWorkspaceRoot(mainWindowCDP);
|
|
104
|
-
if (workspaceRoot) {
|
|
105
|
-
possiblePaths.unshift(path.join(workspaceRoot, filePath));
|
|
106
|
-
}
|
|
107
|
-
}
|
|
219
|
+
// SECURITY: Validate path is within workspace root
|
|
220
|
+
const pathValidation = validatePathWithinRoot(sanitizedPath, workspaceRoot);
|
|
108
221
|
|
|
109
222
|
let content = null;
|
|
110
223
|
let foundPath = null;
|
|
111
224
|
|
|
112
|
-
|
|
225
|
+
if (pathValidation.valid) {
|
|
226
|
+
// Try the validated path first
|
|
113
227
|
try {
|
|
114
|
-
content = await fs.readFile(
|
|
115
|
-
foundPath =
|
|
116
|
-
|
|
117
|
-
|
|
228
|
+
content = await fs.readFile(pathValidation.resolvedPath, 'utf-8');
|
|
229
|
+
foundPath = pathValidation.resolvedPath;
|
|
230
|
+
} catch (e) {
|
|
231
|
+
// File doesn't exist at validated path, try searching
|
|
232
|
+
}
|
|
118
233
|
}
|
|
119
234
|
|
|
120
|
-
//
|
|
235
|
+
// If not found, search within workspace only
|
|
121
236
|
if (!content) {
|
|
122
|
-
|
|
123
|
-
|
|
237
|
+
const fileName = path.basename(sanitizedPath);
|
|
238
|
+
foundPath = await findFileRecursive(workspaceRoot, fileName, MAX_FILE_SEARCH_DEPTH);
|
|
239
|
+
|
|
240
|
+
if (foundPath) {
|
|
241
|
+
// Validate the found path is still within workspace
|
|
242
|
+
const foundValidation = validatePathWithinRoot(foundPath, workspaceRoot);
|
|
243
|
+
if (foundValidation.valid) {
|
|
244
|
+
content = await fs.readFile(foundPath, 'utf-8');
|
|
245
|
+
} else {
|
|
246
|
+
console.warn(`[ReadFile] Found file outside workspace: ${foundPath}`);
|
|
247
|
+
foundPath = null;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
124
250
|
}
|
|
125
251
|
|
|
126
252
|
if (!content) {
|
|
127
|
-
return res.status(404).json({ error: 'File not found' });
|
|
253
|
+
return res.status(404).json({ error: 'File not found within workspace' });
|
|
128
254
|
}
|
|
129
255
|
|
|
130
|
-
const
|
|
131
|
-
const extMap = {
|
|
132
|
-
'ts': 'typescript', 'tsx': 'typescript', 'js': 'javascript', 'jsx': 'javascript',
|
|
133
|
-
'py': 'python', 'html': 'html', 'css': 'css', 'json': 'json', 'md': 'markdown'
|
|
134
|
-
};
|
|
256
|
+
const language = getLanguageFromExtension(sanitizedPath);
|
|
135
257
|
|
|
136
258
|
res.json({
|
|
137
259
|
content,
|
|
138
|
-
fileName: path.basename(
|
|
260
|
+
fileName: path.basename(sanitizedPath),
|
|
139
261
|
fullPath: foundPath,
|
|
140
|
-
language
|
|
262
|
+
language,
|
|
141
263
|
lineCount: content.split('\n').length,
|
|
142
264
|
hasContent: true
|
|
143
265
|
});
|
|
144
266
|
} catch (err) {
|
|
145
|
-
|
|
267
|
+
console.error('[ReadFile] Error:', err.message);
|
|
268
|
+
res.status(500).json({ error: 'Failed to read file' });
|
|
146
269
|
}
|
|
147
270
|
});
|
|
148
271
|
|
|
@@ -158,6 +281,7 @@ export function createApiRouter(cascades, mainWindowCDP) {
|
|
|
158
281
|
console.log(`[Files] Found ${files.length} files in ${workspaceRoot}`);
|
|
159
282
|
res.json({ files, workspaceRoot });
|
|
160
283
|
} catch (err) {
|
|
284
|
+
console.error('[Files] Error:', err.message);
|
|
161
285
|
res.status(500).json({ error: err.message });
|
|
162
286
|
}
|
|
163
287
|
});
|
|
@@ -186,13 +310,16 @@ export function createApiRouter(cascades, mainWindowCDP) {
|
|
|
186
310
|
try {
|
|
187
311
|
const content = await fs.readFile(tasksFilePath, 'utf-8');
|
|
188
312
|
tasks.push({ name: dir.name, path: `.kiro/specs/${dir.name}/tasks.md`, content });
|
|
189
|
-
} catch (e) {
|
|
313
|
+
} catch (e) {
|
|
314
|
+
// Task file doesn't exist, skip
|
|
315
|
+
}
|
|
190
316
|
}
|
|
191
317
|
|
|
192
318
|
tasks.sort((a, b) => a.name.localeCompare(b.name));
|
|
193
319
|
console.log(`[Tasks] Found ${tasks.length} task files`);
|
|
194
320
|
res.json({ tasks, workspaceRoot });
|
|
195
321
|
} catch (err) {
|
|
322
|
+
console.error('[Tasks] Error:', err.message);
|
|
196
323
|
res.status(500).json({ error: err.message });
|
|
197
324
|
}
|
|
198
325
|
});
|
|
@@ -203,12 +330,20 @@ export function createApiRouter(cascades, mainWindowCDP) {
|
|
|
203
330
|
if (!cascade) return res.status(404).json({ error: 'Cascade not found' });
|
|
204
331
|
|
|
205
332
|
const { specName } = req.body;
|
|
206
|
-
if (!specName
|
|
333
|
+
if (!specName || typeof specName !== 'string') {
|
|
334
|
+
return res.status(400).json({ error: 'specName is required and must be a string' });
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Sanitize spec name (alphanumeric, hyphens, underscores only)
|
|
338
|
+
const sanitizedSpecName = specName.replace(/[^a-zA-Z0-9_-]/g, '').substring(0, 100);
|
|
339
|
+
if (!sanitizedSpecName) {
|
|
340
|
+
return res.status(400).json({ error: 'Invalid spec name' });
|
|
341
|
+
}
|
|
207
342
|
|
|
208
343
|
const cdp = cascade.cdp;
|
|
209
344
|
if (!cdp?.rootContextId) return res.status(503).json({ error: 'CDP not connected' });
|
|
210
345
|
|
|
211
|
-
console.log(`[OpenSpec] Opening ${
|
|
346
|
+
console.log(`[OpenSpec] Opening ${sanitizedSpecName}`);
|
|
212
347
|
|
|
213
348
|
// Try to click on spec in sidebar
|
|
214
349
|
const script = `(function() {
|
|
@@ -216,7 +351,7 @@ export function createApiRouter(cascades, mainWindowCDP) {
|
|
|
216
351
|
const activeFrame = document.getElementById('active-frame');
|
|
217
352
|
if (activeFrame && activeFrame.contentDocument) targetDoc = activeFrame.contentDocument;
|
|
218
353
|
|
|
219
|
-
const specName = '${
|
|
354
|
+
const specName = '${sanitizedSpecName}';
|
|
220
355
|
const allElements = targetDoc.querySelectorAll('*');
|
|
221
356
|
|
|
222
357
|
for (const el of allElements) {
|
|
@@ -246,7 +381,12 @@ export function createApiRouter(cascades, mainWindowCDP) {
|
|
|
246
381
|
return router;
|
|
247
382
|
}
|
|
248
383
|
|
|
249
|
-
|
|
384
|
+
/**
|
|
385
|
+
* Get workspace root from VS Code window title
|
|
386
|
+
* Uses environment-based paths instead of hardcoded values
|
|
387
|
+
* @param {object} mainWindowCDP - Main window CDP connection
|
|
388
|
+
* @returns {Promise<string|null>}
|
|
389
|
+
*/
|
|
250
390
|
async function getWorkspaceRoot(mainWindowCDP) {
|
|
251
391
|
if (!mainWindowCDP.connection?.rootContextId) return null;
|
|
252
392
|
|
|
@@ -261,79 +401,115 @@ async function getWorkspaceRoot(mainWindowCDP) {
|
|
|
261
401
|
const parts = title.split(' - ');
|
|
262
402
|
if (parts.length >= 2) {
|
|
263
403
|
const folderName = parts[parts.length - 2].trim();
|
|
404
|
+
const homeDir = os.homedir();
|
|
405
|
+
|
|
406
|
+
// Build possible roots from environment, not hardcoded paths
|
|
264
407
|
const possibleRoots = [
|
|
265
408
|
process.cwd(),
|
|
266
|
-
path.join(
|
|
267
|
-
path.join(
|
|
268
|
-
path.join(
|
|
269
|
-
path.join(
|
|
270
|
-
path.join(
|
|
409
|
+
path.join(homeDir, folderName),
|
|
410
|
+
path.join(homeDir, 'projects', folderName),
|
|
411
|
+
path.join(homeDir, 'dev', folderName),
|
|
412
|
+
path.join(homeDir, 'workspace', folderName),
|
|
413
|
+
path.join(homeDir, 'code', folderName)
|
|
271
414
|
];
|
|
272
415
|
|
|
416
|
+
// Add Windows-specific paths if on Windows
|
|
417
|
+
if (process.platform === 'win32') {
|
|
418
|
+
const drives = ['C:', 'D:', 'E:'];
|
|
419
|
+
for (const drive of drives) {
|
|
420
|
+
possibleRoots.push(
|
|
421
|
+
path.join(drive, 'dev', folderName),
|
|
422
|
+
path.join(drive, 'projects', folderName),
|
|
423
|
+
path.join(drive, 'workspace', folderName)
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
273
428
|
for (const root of possibleRoots) {
|
|
274
429
|
try {
|
|
275
430
|
const stat = await fs.stat(root);
|
|
276
431
|
if (stat.isDirectory()) {
|
|
277
432
|
const hasKiro = await fs.access(path.join(root, '.kiro')).then(() => true).catch(() => false);
|
|
278
433
|
const hasPackage = await fs.access(path.join(root, 'package.json')).then(() => true).catch(() => false);
|
|
279
|
-
|
|
434
|
+
const hasGit = await fs.access(path.join(root, '.git')).then(() => true).catch(() => false);
|
|
435
|
+
if (hasKiro || hasPackage || hasGit) return root;
|
|
280
436
|
}
|
|
281
|
-
} catch (e) {
|
|
437
|
+
} catch (e) {
|
|
438
|
+
// Path doesn't exist, continue
|
|
439
|
+
}
|
|
282
440
|
}
|
|
283
441
|
}
|
|
284
|
-
} catch (e) {
|
|
442
|
+
} catch (e) {
|
|
443
|
+
console.error('[getWorkspaceRoot] Error:', e.message);
|
|
444
|
+
}
|
|
285
445
|
return null;
|
|
286
446
|
}
|
|
287
447
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
448
|
+
/**
|
|
449
|
+
* Find file recursively within a directory
|
|
450
|
+
* @param {string} dir - Directory to search
|
|
451
|
+
* @param {string} fileName - File name to find
|
|
452
|
+
* @param {number} maxDepth - Maximum search depth
|
|
453
|
+
* @param {number} currentDepth - Current depth (internal)
|
|
454
|
+
* @returns {Promise<string|null>}
|
|
455
|
+
*/
|
|
456
|
+
async function findFileRecursive(dir, fileName, maxDepth = MAX_FILE_SEARCH_DEPTH, currentDepth = 0) {
|
|
457
|
+
if (currentDepth > maxDepth) return null;
|
|
291
458
|
|
|
292
459
|
try {
|
|
293
460
|
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
294
461
|
|
|
462
|
+
// Check files first
|
|
295
463
|
for (const entry of entries) {
|
|
296
464
|
if (entry.isFile() && entry.name === fileName) {
|
|
297
465
|
return path.join(dir, entry.name);
|
|
298
466
|
}
|
|
299
467
|
}
|
|
300
468
|
|
|
469
|
+
// Then recurse into directories
|
|
301
470
|
for (const entry of entries) {
|
|
302
471
|
if (entry.isDirectory() &&
|
|
303
472
|
(!entry.name.startsWith('.') || entry.name === '.kiro') &&
|
|
304
|
-
entry.name !== 'node_modules' &&
|
|
305
|
-
|
|
473
|
+
entry.name !== 'node_modules' &&
|
|
474
|
+
entry.name !== 'dist' &&
|
|
475
|
+
entry.name !== 'build' &&
|
|
476
|
+
entry.name !== '.git') {
|
|
477
|
+
const found = await findFileRecursive(
|
|
478
|
+
path.join(dir, entry.name),
|
|
479
|
+
fileName,
|
|
480
|
+
maxDepth,
|
|
481
|
+
currentDepth + 1
|
|
482
|
+
);
|
|
306
483
|
if (found) return found;
|
|
307
484
|
}
|
|
308
485
|
}
|
|
309
|
-
} catch (e) {
|
|
486
|
+
} catch (e) {
|
|
487
|
+
// Directory not accessible, skip
|
|
488
|
+
}
|
|
310
489
|
return null;
|
|
311
490
|
}
|
|
312
491
|
|
|
313
|
-
|
|
492
|
+
/**
|
|
493
|
+
* Collect workspace files for file tree
|
|
494
|
+
* @param {string} workspaceRoot - Workspace root directory
|
|
495
|
+
* @returns {Promise<Array<{name: string, path: string, language: string}>>}
|
|
496
|
+
*/
|
|
314
497
|
async function collectWorkspaceFiles(workspaceRoot) {
|
|
315
|
-
const codeExtensions = new Set([
|
|
316
|
-
'.ts', '.tsx', '.js', '.jsx', '.py', '.java', '.go', '.rs',
|
|
317
|
-
'.html', '.css', '.scss', '.json', '.yaml', '.yml', '.md',
|
|
318
|
-
'.sql', '.sh', '.c', '.cpp', '.h', '.cs', '.vue', '.svelte'
|
|
319
|
-
]);
|
|
320
|
-
|
|
321
|
-
const extToLang = {
|
|
322
|
-
'.ts': 'typescript', '.tsx': 'typescript', '.js': 'javascript', '.jsx': 'javascript',
|
|
323
|
-
'.py': 'python', '.html': 'html', '.css': 'css', '.json': 'json', '.md': 'markdown'
|
|
324
|
-
};
|
|
325
|
-
|
|
326
498
|
const files = [];
|
|
327
499
|
|
|
328
500
|
async function collect(dir, relativePath = '', depth = 0) {
|
|
329
|
-
if (depth >
|
|
501
|
+
if (depth > MAX_WORKSPACE_DEPTH) return;
|
|
330
502
|
|
|
331
503
|
try {
|
|
332
504
|
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
333
505
|
|
|
334
506
|
for (const entry of entries) {
|
|
507
|
+
// Skip hidden files (except .kiro and .github), node_modules, and build directories
|
|
335
508
|
if ((entry.name.startsWith('.') && entry.name !== '.kiro' && entry.name !== '.github') ||
|
|
336
|
-
entry.name === 'node_modules' ||
|
|
509
|
+
entry.name === 'node_modules' ||
|
|
510
|
+
entry.name === 'dist' ||
|
|
511
|
+
entry.name === 'build' ||
|
|
512
|
+
entry.name === '__pycache__') {
|
|
337
513
|
continue;
|
|
338
514
|
}
|
|
339
515
|
|
|
@@ -341,15 +517,20 @@ async function collectWorkspaceFiles(workspaceRoot) {
|
|
|
341
517
|
const entryRelative = relativePath ? `${relativePath}/${entry.name}` : entry.name;
|
|
342
518
|
|
|
343
519
|
if (entry.isFile()) {
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
520
|
+
if (isCodeFile(entry.name)) {
|
|
521
|
+
files.push({
|
|
522
|
+
name: entry.name,
|
|
523
|
+
path: entryRelative,
|
|
524
|
+
language: getLanguageFromExtension(entry.name)
|
|
525
|
+
});
|
|
347
526
|
}
|
|
348
527
|
} else if (entry.isDirectory()) {
|
|
349
528
|
await collect(entryPath, entryRelative, depth + 1);
|
|
350
529
|
}
|
|
351
530
|
}
|
|
352
|
-
} catch (e) {
|
|
531
|
+
} catch (e) {
|
|
532
|
+
// Directory not accessible, skip
|
|
533
|
+
}
|
|
353
534
|
}
|
|
354
535
|
|
|
355
536
|
await collect(workspaceRoot);
|
package/src/server.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
3
|
* Kiro Mobile Bridge Server
|
|
4
|
+
* @version 1.0.10
|
|
4
5
|
*
|
|
5
6
|
* A mobile web interface for monitoring Kiro IDE agent sessions from your phone over LAN.
|
|
6
7
|
* Captures snapshots of the chat interface via CDP and lets you send messages remotely.
|
|
@@ -19,6 +20,14 @@ import { captureMetadata, captureCSS, captureSnapshot, captureEditor } from './s
|
|
|
19
20
|
// Utils
|
|
20
21
|
import { generateId, computeHash } from './utils/hash.js';
|
|
21
22
|
import { getLocalIP } from './utils/network.js';
|
|
23
|
+
import {
|
|
24
|
+
CDP_PORTS,
|
|
25
|
+
DISCOVERY_INTERVAL_ACTIVE,
|
|
26
|
+
DISCOVERY_INTERVAL_STABLE,
|
|
27
|
+
SNAPSHOT_INTERVAL_ACTIVE,
|
|
28
|
+
SNAPSHOT_INTERVAL_IDLE,
|
|
29
|
+
SNAPSHOT_IDLE_THRESHOLD
|
|
30
|
+
} from './utils/constants.js';
|
|
22
31
|
|
|
23
32
|
// Routes
|
|
24
33
|
import { createApiRouter } from './routes/api.js';
|
|
@@ -31,7 +40,6 @@ const __dirname = dirname(__filename);
|
|
|
31
40
|
// =============================================================================
|
|
32
41
|
|
|
33
42
|
const PORT = process.env.PORT || 3000;
|
|
34
|
-
const CDP_PORTS = [9000, 9001, 9002, 9003, 9222, 9229];
|
|
35
43
|
|
|
36
44
|
// =============================================================================
|
|
37
45
|
// State Management
|
|
@@ -44,12 +52,12 @@ const pollingState = {
|
|
|
44
52
|
lastCascadeCount: 0,
|
|
45
53
|
lastMainWindowConnected: false,
|
|
46
54
|
discoveryInterval: null,
|
|
47
|
-
discoveryIntervalMs:
|
|
55
|
+
discoveryIntervalMs: DISCOVERY_INTERVAL_ACTIVE,
|
|
48
56
|
stableCount: 0,
|
|
49
57
|
snapshotInterval: null,
|
|
50
|
-
snapshotIntervalMs:
|
|
58
|
+
snapshotIntervalMs: SNAPSHOT_INTERVAL_ACTIVE,
|
|
51
59
|
lastSnapshotChange: Date.now(),
|
|
52
|
-
idleThreshold:
|
|
60
|
+
idleThreshold: SNAPSHOT_IDLE_THRESHOLD
|
|
53
61
|
};
|
|
54
62
|
|
|
55
63
|
// =============================================================================
|
|
@@ -113,7 +121,6 @@ async function discoverTargets() {
|
|
|
113
121
|
foundCascadeIds.add(cascadeId);
|
|
114
122
|
|
|
115
123
|
if (!cascades.has(cascadeId)) {
|
|
116
|
-
console.log(`[Discovery] Found new Kiro Agent: ${target.title} (${cascadeId})`);
|
|
117
124
|
stateChanged = true;
|
|
118
125
|
|
|
119
126
|
try {
|
|
@@ -145,7 +152,10 @@ async function discoverTargets() {
|
|
|
145
152
|
cascades.get(cascadeId).metadata.windowTitle = target.title || cascades.get(cascadeId).metadata.windowTitle;
|
|
146
153
|
}
|
|
147
154
|
}
|
|
148
|
-
} catch (err) {
|
|
155
|
+
} catch (err) {
|
|
156
|
+
// Log port scanning errors for debugging
|
|
157
|
+
console.debug(`[Discovery] Error scanning port ${port}: ${err.message}`);
|
|
158
|
+
}
|
|
149
159
|
}
|
|
150
160
|
|
|
151
161
|
// Clean up disconnected targets
|
|
@@ -153,7 +163,11 @@ async function discoverTargets() {
|
|
|
153
163
|
if (!foundCascadeIds.has(cascadeId)) {
|
|
154
164
|
console.log(`[Discovery] Target no longer available: ${cascadeId}`);
|
|
155
165
|
stateChanged = true;
|
|
156
|
-
try {
|
|
166
|
+
try {
|
|
167
|
+
cascade.cdp.close();
|
|
168
|
+
} catch (e) {
|
|
169
|
+
console.debug(`[Discovery] Error closing cascade ${cascadeId}: ${e.message}`);
|
|
170
|
+
}
|
|
157
171
|
cascades.delete(cascadeId);
|
|
158
172
|
broadcastCascadeList();
|
|
159
173
|
}
|
|
@@ -206,8 +220,10 @@ async function pollSnapshots() {
|
|
|
206
220
|
}
|
|
207
221
|
|
|
208
222
|
// Capture editor from main window
|
|
223
|
+
// Store rootContextId locally to avoid race conditions during async operations
|
|
209
224
|
const mainCDP = mainWindowCDP.connection;
|
|
210
|
-
|
|
225
|
+
const contextId = mainCDP?.rootContextId;
|
|
226
|
+
if (mainCDP && contextId) {
|
|
211
227
|
const editor = await captureEditor(mainCDP);
|
|
212
228
|
if (editor?.hasContent) {
|
|
213
229
|
const editorHash = computeHash(editor.content + editor.fileName);
|
|
@@ -239,14 +255,14 @@ async function pollSnapshots() {
|
|
|
239
255
|
function adjustDiscoveryInterval(hasChanges) {
|
|
240
256
|
if (hasChanges) {
|
|
241
257
|
pollingState.stableCount = 0;
|
|
242
|
-
if (pollingState.discoveryIntervalMs !==
|
|
243
|
-
pollingState.discoveryIntervalMs =
|
|
258
|
+
if (pollingState.discoveryIntervalMs !== DISCOVERY_INTERVAL_ACTIVE) {
|
|
259
|
+
pollingState.discoveryIntervalMs = DISCOVERY_INTERVAL_ACTIVE;
|
|
244
260
|
restartDiscoveryInterval();
|
|
245
261
|
}
|
|
246
262
|
} else {
|
|
247
263
|
pollingState.stableCount++;
|
|
248
|
-
if (pollingState.stableCount >= 3 && pollingState.discoveryIntervalMs !==
|
|
249
|
-
pollingState.discoveryIntervalMs =
|
|
264
|
+
if (pollingState.stableCount >= 3 && pollingState.discoveryIntervalMs !== DISCOVERY_INTERVAL_STABLE) {
|
|
265
|
+
pollingState.discoveryIntervalMs = DISCOVERY_INTERVAL_STABLE;
|
|
250
266
|
restartDiscoveryInterval();
|
|
251
267
|
console.log('[Discovery] Stable state, slowing to 30s interval');
|
|
252
268
|
}
|
|
@@ -262,14 +278,14 @@ function adjustSnapshotInterval(hasChanges) {
|
|
|
262
278
|
const now = Date.now();
|
|
263
279
|
if (hasChanges) {
|
|
264
280
|
pollingState.lastSnapshotChange = now;
|
|
265
|
-
if (pollingState.snapshotIntervalMs !==
|
|
266
|
-
pollingState.snapshotIntervalMs =
|
|
281
|
+
if (pollingState.snapshotIntervalMs !== SNAPSHOT_INTERVAL_ACTIVE) {
|
|
282
|
+
pollingState.snapshotIntervalMs = SNAPSHOT_INTERVAL_ACTIVE;
|
|
267
283
|
restartSnapshotInterval();
|
|
268
284
|
}
|
|
269
285
|
} else {
|
|
270
286
|
const idleTime = now - pollingState.lastSnapshotChange;
|
|
271
|
-
if (idleTime > pollingState.idleThreshold && pollingState.snapshotIntervalMs !==
|
|
272
|
-
pollingState.snapshotIntervalMs =
|
|
287
|
+
if (idleTime > pollingState.idleThreshold && pollingState.snapshotIntervalMs !== SNAPSHOT_INTERVAL_IDLE) {
|
|
288
|
+
pollingState.snapshotIntervalMs = SNAPSHOT_INTERVAL_IDLE;
|
|
273
289
|
restartSnapshotInterval();
|
|
274
290
|
}
|
|
275
291
|
}
|
|
@@ -314,7 +330,7 @@ function broadcastCascadeList() {
|
|
|
314
330
|
// =============================================================================
|
|
315
331
|
|
|
316
332
|
const app = express();
|
|
317
|
-
app.use(express.json());
|
|
333
|
+
app.use(express.json({ limit: '1mb' })); // Limit request body size
|
|
318
334
|
app.use(express.static(join(__dirname, 'public')));
|
|
319
335
|
|
|
320
336
|
// Mount API routes
|