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,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
|
+
}
|