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,358 @@
1
+ /**
2
+ * API Routes - REST endpoints for mobile client
3
+ */
4
+ import { Router } from 'express';
5
+ import fs from 'fs/promises';
6
+ import path from 'path';
7
+ import { injectMessage } from '../services/message.js';
8
+ import { clickElement } from '../services/click.js';
9
+
10
+ /**
11
+ * Create API router
12
+ * @param {Map} cascades - Cascade connections map
13
+ * @param {object} mainWindowCDP - Main window CDP connection
14
+ * @returns {Router} - Express router
15
+ */
16
+ export function createApiRouter(cascades, mainWindowCDP) {
17
+ const router = Router();
18
+
19
+ // GET /snapshot/:id - Get HTML snapshot for a cascade
20
+ router.get('/snapshot/:id', (req, res) => {
21
+ const cascade = cascades.get(req.params.id);
22
+ if (!cascade) return res.status(404).json({ error: 'Cascade not found' });
23
+ if (!cascade.snapshot) return res.status(404).json({ error: 'No snapshot available' });
24
+ res.json(cascade.snapshot);
25
+ });
26
+
27
+ // GET /styles/:id - Get CSS for a cascade
28
+ router.get('/styles/:id', (req, res) => {
29
+ const cascade = cascades.get(req.params.id);
30
+ if (!cascade) return res.status(404).json({ error: 'Cascade not found' });
31
+ if (!cascade.css) return res.status(404).json({ error: 'No styles available' });
32
+ res.type('text/css').send(cascade.css);
33
+ });
34
+
35
+ // GET /editor/:id - Get editor snapshot
36
+ router.get('/editor/:id', (req, res) => {
37
+ const cascade = cascades.get(req.params.id);
38
+ if (!cascade) return res.status(404).json({ error: 'Cascade not found' });
39
+ if (!cascade.editor?.hasContent) return res.status(404).json({ error: 'No editor content available' });
40
+ res.json(cascade.editor);
41
+ });
42
+
43
+ // POST /send/:id - Send message to chat
44
+ router.post('/send/:id', async (req, res) => {
45
+ const cascade = cascades.get(req.params.id);
46
+ if (!cascade) return res.status(404).json({ error: 'Cascade not found' });
47
+
48
+ const { message } = req.body;
49
+ if (!message || typeof message !== 'string') {
50
+ return res.status(400).json({ error: 'Message is required' });
51
+ }
52
+
53
+ if (!cascade.cdp) return res.status(503).json({ error: 'CDP connection not available' });
54
+
55
+ console.log(`[Send] Message to ${req.params.id}: ${message.substring(0, 50)}...`);
56
+ const result = await injectMessage(cascade.cdp, message);
57
+
58
+ if (result.success) {
59
+ res.json({ success: true, method: result.method });
60
+ } else {
61
+ res.status(500).json({ success: false, error: result.error });
62
+ }
63
+ });
64
+
65
+ // POST /click/:id - Click UI element
66
+ router.post('/click/:id', async (req, res) => {
67
+ const cascade = cascades.get(req.params.id);
68
+ if (!cascade) return res.status(404).json({ error: 'Cascade not found' });
69
+ if (!cascade.cdp?.rootContextId) return res.status(503).json({ error: 'CDP not available' });
70
+
71
+ const clickInfo = req.body;
72
+ console.log(`[Click] ${clickInfo.text?.substring(0, 30) || clickInfo.ariaLabel || clickInfo.tag}`);
73
+
74
+ try {
75
+ const result = await clickElement(cascade.cdp, clickInfo);
76
+ res.json(result);
77
+ } catch (err) {
78
+ res.status(500).json({ success: false, error: err.message });
79
+ }
80
+ });
81
+
82
+ // POST /readFile/:id - Read file from filesystem
83
+ router.post('/readFile/:id', async (req, res) => {
84
+ const cascade = cascades.get(req.params.id);
85
+ if (!cascade) return res.status(404).json({ error: 'Cascade not found' });
86
+
87
+ const { filePath } = req.body;
88
+ if (!filePath) return res.status(400).json({ error: 'filePath is required' });
89
+
90
+ console.log(`[ReadFile] ${filePath}`);
91
+
92
+ 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
+ ];
100
+
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
+ }
108
+
109
+ let content = null;
110
+ let foundPath = null;
111
+
112
+ for (const tryPath of possiblePaths) {
113
+ try {
114
+ content = await fs.readFile(tryPath, 'utf-8');
115
+ foundPath = tryPath;
116
+ break;
117
+ } catch (e) {}
118
+ }
119
+
120
+ // Recursive search fallback
121
+ if (!content) {
122
+ foundPath = await findFileRecursive(process.cwd(), fileName);
123
+ if (foundPath) content = await fs.readFile(foundPath, 'utf-8');
124
+ }
125
+
126
+ if (!content) {
127
+ return res.status(404).json({ error: 'File not found' });
128
+ }
129
+
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
+ };
135
+
136
+ res.json({
137
+ content,
138
+ fileName: path.basename(filePath),
139
+ fullPath: foundPath,
140
+ language: extMap[ext] || ext,
141
+ lineCount: content.split('\n').length,
142
+ hasContent: true
143
+ });
144
+ } catch (err) {
145
+ res.status(500).json({ error: err.message });
146
+ }
147
+ });
148
+
149
+ // GET /files/:id - List workspace files
150
+ router.get('/files/:id', async (req, res) => {
151
+ const cascade = cascades.get(req.params.id);
152
+ if (!cascade) return res.status(404).json({ error: 'Cascade not found' });
153
+
154
+ try {
155
+ const workspaceRoot = await getWorkspaceRoot(mainWindowCDP) || process.cwd();
156
+ const files = await collectWorkspaceFiles(workspaceRoot);
157
+
158
+ console.log(`[Files] Found ${files.length} files in ${workspaceRoot}`);
159
+ res.json({ files, workspaceRoot });
160
+ } catch (err) {
161
+ res.status(500).json({ error: err.message });
162
+ }
163
+ });
164
+
165
+ // GET /tasks/:id - List task files from .kiro/specs
166
+ router.get('/tasks/:id', async (req, res) => {
167
+ const cascade = cascades.get(req.params.id);
168
+ if (!cascade) return res.status(404).json({ error: 'Cascade not found' });
169
+
170
+ try {
171
+ const workspaceRoot = await getWorkspaceRoot(mainWindowCDP) || process.cwd();
172
+ const kiroSpecsPath = path.join(workspaceRoot, '.kiro', 'specs');
173
+
174
+ try {
175
+ await fs.access(kiroSpecsPath);
176
+ } catch (e) {
177
+ return res.json({ tasks: [], workspaceRoot });
178
+ }
179
+
180
+ const tasks = [];
181
+ const specDirs = await fs.readdir(kiroSpecsPath, { withFileTypes: true });
182
+
183
+ for (const dir of specDirs) {
184
+ if (!dir.isDirectory()) continue;
185
+ const tasksFilePath = path.join(kiroSpecsPath, dir.name, 'tasks.md');
186
+ try {
187
+ const content = await fs.readFile(tasksFilePath, 'utf-8');
188
+ tasks.push({ name: dir.name, path: `.kiro/specs/${dir.name}/tasks.md`, content });
189
+ } catch (e) {}
190
+ }
191
+
192
+ tasks.sort((a, b) => a.name.localeCompare(b.name));
193
+ console.log(`[Tasks] Found ${tasks.length} task files`);
194
+ res.json({ tasks, workspaceRoot });
195
+ } catch (err) {
196
+ res.status(500).json({ error: err.message });
197
+ }
198
+ });
199
+
200
+ // POST /open-spec/:id - Open spec in Kiro
201
+ router.post('/open-spec/:id', async (req, res) => {
202
+ const cascade = cascades.get(req.params.id);
203
+ if (!cascade) return res.status(404).json({ error: 'Cascade not found' });
204
+
205
+ const { specName } = req.body;
206
+ if (!specName) return res.status(400).json({ error: 'specName is required' });
207
+
208
+ const cdp = cascade.cdp;
209
+ if (!cdp?.rootContextId) return res.status(503).json({ error: 'CDP not connected' });
210
+
211
+ console.log(`[OpenSpec] Opening ${specName}`);
212
+
213
+ // Try to click on spec in sidebar
214
+ const script = `(function() {
215
+ let targetDoc = document;
216
+ const activeFrame = document.getElementById('active-frame');
217
+ if (activeFrame && activeFrame.contentDocument) targetDoc = activeFrame.contentDocument;
218
+
219
+ const specName = '${specName.replace(/'/g, "\\'")}';
220
+ const allElements = targetDoc.querySelectorAll('*');
221
+
222
+ for (const el of allElements) {
223
+ const text = (el.textContent || '').trim();
224
+ if (text === specName || text.includes(specName)) {
225
+ if (el.offsetParent !== null && el.textContent.length < 100) {
226
+ el.click();
227
+ return { success: true, method: 'click-spec-name' };
228
+ }
229
+ }
230
+ }
231
+ return { success: false, error: 'Spec not found in UI' };
232
+ })()`;
233
+
234
+ try {
235
+ const result = await cdp.call('Runtime.evaluate', {
236
+ expression: script,
237
+ contextId: cdp.rootContextId,
238
+ returnByValue: true
239
+ });
240
+ res.json(result.result?.value || { success: false });
241
+ } catch (err) {
242
+ res.status(500).json({ success: false, error: err.message });
243
+ }
244
+ });
245
+
246
+ return router;
247
+ }
248
+
249
+ // Helper: Get workspace root from VS Code window title
250
+ async function getWorkspaceRoot(mainWindowCDP) {
251
+ if (!mainWindowCDP.connection?.rootContextId) return null;
252
+
253
+ try {
254
+ const result = await mainWindowCDP.connection.call('Runtime.evaluate', {
255
+ expression: 'document.title',
256
+ contextId: mainWindowCDP.connection.rootContextId,
257
+ returnByValue: true
258
+ });
259
+
260
+ const title = result.result?.value || '';
261
+ const parts = title.split(' - ');
262
+ if (parts.length >= 2) {
263
+ const folderName = parts[parts.length - 2].trim();
264
+ const possibleRoots = [
265
+ 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)
271
+ ];
272
+
273
+ for (const root of possibleRoots) {
274
+ try {
275
+ const stat = await fs.stat(root);
276
+ if (stat.isDirectory()) {
277
+ const hasKiro = await fs.access(path.join(root, '.kiro')).then(() => true).catch(() => false);
278
+ const hasPackage = await fs.access(path.join(root, 'package.json')).then(() => true).catch(() => false);
279
+ if (hasKiro || hasPackage) return root;
280
+ }
281
+ } catch (e) {}
282
+ }
283
+ }
284
+ } catch (e) {}
285
+ return null;
286
+ }
287
+
288
+ // Helper: Find file recursively
289
+ async function findFileRecursive(dir, fileName, maxDepth = 4, depth = 0) {
290
+ if (depth > maxDepth) return null;
291
+
292
+ try {
293
+ const entries = await fs.readdir(dir, { withFileTypes: true });
294
+
295
+ for (const entry of entries) {
296
+ if (entry.isFile() && entry.name === fileName) {
297
+ return path.join(dir, entry.name);
298
+ }
299
+ }
300
+
301
+ for (const entry of entries) {
302
+ if (entry.isDirectory() &&
303
+ (!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);
306
+ if (found) return found;
307
+ }
308
+ }
309
+ } catch (e) {}
310
+ return null;
311
+ }
312
+
313
+ // Helper: Collect workspace files
314
+ 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
+ const files = [];
327
+
328
+ async function collect(dir, relativePath = '', depth = 0) {
329
+ if (depth > 5) return;
330
+
331
+ try {
332
+ const entries = await fs.readdir(dir, { withFileTypes: true });
333
+
334
+ for (const entry of entries) {
335
+ if ((entry.name.startsWith('.') && entry.name !== '.kiro' && entry.name !== '.github') ||
336
+ entry.name === 'node_modules' || entry.name === 'dist' || entry.name === 'build') {
337
+ continue;
338
+ }
339
+
340
+ const entryPath = path.join(dir, entry.name);
341
+ const entryRelative = relativePath ? `${relativePath}/${entry.name}` : entry.name;
342
+
343
+ 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) });
347
+ }
348
+ } else if (entry.isDirectory()) {
349
+ await collect(entryPath, entryRelative, depth + 1);
350
+ }
351
+ }
352
+ } catch (e) {}
353
+ }
354
+
355
+ await collect(workspaceRoot);
356
+ files.sort((a, b) => a.path.localeCompare(b.path));
357
+ return files;
358
+ }