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/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
- if (!message || typeof message !== 'string') {
50
- return res.status(400).json({ error: 'Message is required' });
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
- console.log(`[Send] Message to ${req.params.id}: ${message.substring(0, 50)}...`);
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
- const clickInfo = req.body;
72
- console.log(`[Click] ${clickInfo.text?.substring(0, 30) || clickInfo.ariaLabel || clickInfo.tag}`);
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, clickInfo);
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) return res.status(400).json({ error: 'filePath is required' });
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
- console.log(`[ReadFile] ${filePath}`);
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
- const fileName = path.basename(filePath);
94
- const possiblePaths = [
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
- // Add workspace-relative paths
102
- if (!path.isAbsolute(filePath)) {
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
- for (const tryPath of possiblePaths) {
225
+ if (pathValidation.valid) {
226
+ // Try the validated path first
113
227
  try {
114
- content = await fs.readFile(tryPath, 'utf-8');
115
- foundPath = tryPath;
116
- break;
117
- } catch (e) {}
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
- // Recursive search fallback
235
+ // If not found, search within workspace only
121
236
  if (!content) {
122
- foundPath = await findFileRecursive(process.cwd(), fileName);
123
- if (foundPath) content = await fs.readFile(foundPath, 'utf-8');
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 ext = path.extname(filePath).toLowerCase().slice(1);
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(filePath),
260
+ fileName: path.basename(sanitizedPath),
139
261
  fullPath: foundPath,
140
- language: extMap[ext] || ext,
262
+ language,
141
263
  lineCount: content.split('\n').length,
142
264
  hasContent: true
143
265
  });
144
266
  } catch (err) {
145
- res.status(500).json({ error: err.message });
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) return res.status(400).json({ error: 'specName is required' });
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 ${specName}`);
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 = '${specName.replace(/'/g, "\\'")}';
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
- // Helper: Get workspace root from VS Code window title
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(process.env.HOME || process.env.USERPROFILE || '', folderName),
267
- path.join(process.env.HOME || process.env.USERPROFILE || '', 'projects', folderName),
268
- path.join(process.env.HOME || process.env.USERPROFILE || '', 'dev', folderName),
269
- path.join('C:', 'gab', folderName),
270
- path.join('C:', 'dev', folderName)
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
- if (hasKiro || hasPackage) return root;
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
- // Helper: Find file recursively
289
- async function findFileRecursive(dir, fileName, maxDepth = 4, depth = 0) {
290
- if (depth > maxDepth) return null;
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' && entry.name !== 'dist') {
305
- const found = await findFileRecursive(path.join(dir, entry.name), fileName, maxDepth, depth + 1);
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
- // Helper: Collect workspace files
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 > 5) return;
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' || entry.name === 'dist' || entry.name === 'build') {
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
- const ext = path.extname(entry.name).toLowerCase();
345
- if (codeExtensions.has(ext)) {
346
- files.push({ name: entry.name, path: entryRelative, language: extToLang[ext] || ext.slice(1) });
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: 10000,
55
+ discoveryIntervalMs: DISCOVERY_INTERVAL_ACTIVE,
48
56
  stableCount: 0,
49
57
  snapshotInterval: null,
50
- snapshotIntervalMs: 1000,
58
+ snapshotIntervalMs: SNAPSHOT_INTERVAL_ACTIVE,
51
59
  lastSnapshotChange: Date.now(),
52
- idleThreshold: 10000
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 { cascade.cdp.close(); } catch (e) {}
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
- if (mainCDP?.rootContextId) {
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 !== 10000) {
243
- pollingState.discoveryIntervalMs = 10000;
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 !== 30000) {
249
- pollingState.discoveryIntervalMs = 30000;
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 !== 1000) {
266
- pollingState.snapshotIntervalMs = 1000;
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 !== 3000) {
272
- pollingState.snapshotIntervalMs = 3000;
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