theus-cli-linux-arm64 1.2.1 → 1.2.2
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 +15 -0
- package/RELEASE.json +6 -6
- package/bin/theus +0 -0
- package/package.json +1 -1
- package/vendor/chrome-extension/README.md +142 -0
- package/vendor/chrome-extension/background.js +540 -0
- package/vendor/chrome-extension/cdp-core.js +400 -0
- package/vendor/chrome-extension/cdp-core.test.ts +321 -0
- package/vendor/chrome-extension/cdp.js +672 -0
- package/vendor/chrome-extension/icons/icon128.png +0 -0
- package/vendor/chrome-extension/icons/icon16.png +0 -0
- package/vendor/chrome-extension/icons/icon32.png +0 -0
- package/vendor/chrome-extension/icons/icon48.png +0 -0
- package/vendor/chrome-extension/manifest.json +36 -0
- package/vendor/chrome-extension/popup.html +78 -0
- package/vendor/chrome-extension/popup.js +47 -0
- package/vendor/chrome-extension/recorder.js +316 -0
- package/vendor/theus-arsenal/.theus-plugin/plugin.json +1 -1
- package/vendor/theus-arsenal/README.md +1 -1
|
@@ -0,0 +1,540 @@
|
|
|
1
|
+
// Theus en Chrome — service worker (MV3).
|
|
2
|
+
//
|
|
3
|
+
// Conecta con el host de mensajería nativa que sirve el CLI de Theus
|
|
4
|
+
// (`com.theus.theus_code_browser_extension` → wrapper que ejecuta
|
|
5
|
+
// `theus --chrome-native-host`) y atiende los `tool_request` que el CLI
|
|
6
|
+
// serializa por el socket del host. Contrato de mensajes (ver
|
|
7
|
+
// utils/theusInChrome/chromeNativeHost.ts en el repo del CLI):
|
|
8
|
+
//
|
|
9
|
+
// host → extensión: {type:'tool_request', method, params:{id,name,arguments}}
|
|
10
|
+
// {type:'pong'|'status_response'|'mcp_connected'|'mcp_disconnected'|'error'}
|
|
11
|
+
// extensión → host: {type:'tool_response', id, result|error}
|
|
12
|
+
// {type:'ping'} · {type:'get_status'}
|
|
13
|
+
//
|
|
14
|
+
// Límite del canal host↔CLI: 16 MB por mensaje (LIMITE_CANAL_BYTES en cdp.js;
|
|
15
|
+
// debe casar con MAX_CHANNEL_MESSAGE_BYTES del CLI). Los textos largos se
|
|
16
|
+
// truncan igualmente (MAX_TEXT_CHARS) y las capturas se recomprimen.
|
|
17
|
+
//
|
|
18
|
+
// Fase 2 (control total vía chrome.debugger): cdp-core.js (lógica pura) +
|
|
19
|
+
// cdp.js (sesiones CDP por pestaña, buffers y handlers). Se fusionan en
|
|
20
|
+
// TOOL_HANDLERS más abajo.
|
|
21
|
+
importScripts('cdp-core.js', 'cdp.js')
|
|
22
|
+
|
|
23
|
+
const NATIVE_HOST = 'com.theus.theus_code_browser_extension'
|
|
24
|
+
const PING_INTERVAL_MS = 20_000 // mantiene vivo el service worker y el canal
|
|
25
|
+
const NAVIGATE_TIMEOUT_MS = 45_000
|
|
26
|
+
const MAX_TEXT_CHARS = 180_000 // margen holgado bajo el límite de 1 MB
|
|
27
|
+
const MAX_FIND_RESULTS = 25
|
|
28
|
+
|
|
29
|
+
let port = null
|
|
30
|
+
let reconnectDelayMs = 1_000
|
|
31
|
+
let reconnectTimer = null
|
|
32
|
+
let pingTimer = null
|
|
33
|
+
let mcpClientCount = 0 // clientes MCP (sesiones del CLI) vistos por el host
|
|
34
|
+
let lastError = null
|
|
35
|
+
|
|
36
|
+
// Kuti fase 1 — estado del grabador. Expuesto en globalThis para que cdp.js
|
|
37
|
+
// (cargado por importScripts, mismo ámbito global) sepa si debe reportar las
|
|
38
|
+
// navegaciones del frame principal.
|
|
39
|
+
const RecorderState = { grabando: false, tabGrabada: null, capturas: false }
|
|
40
|
+
globalThis.__kuti = RecorderState
|
|
41
|
+
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// Conexión con el host nativo
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
function connectToHost() {
|
|
47
|
+
if (port) return
|
|
48
|
+
try {
|
|
49
|
+
port = chrome.runtime.connectNative(NATIVE_HOST)
|
|
50
|
+
} catch (e) {
|
|
51
|
+
lastError = String(e)
|
|
52
|
+
scheduleReconnect()
|
|
53
|
+
return
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
port.onMessage.addListener(onHostMessage)
|
|
57
|
+
port.onDisconnect.addListener(() => {
|
|
58
|
+
lastError = chrome.runtime.lastError
|
|
59
|
+
? chrome.runtime.lastError.message
|
|
60
|
+
: null
|
|
61
|
+
port = null
|
|
62
|
+
mcpClientCount = 0
|
|
63
|
+
stopPing()
|
|
64
|
+
scheduleReconnect()
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
reconnectDelayMs = 1_000
|
|
68
|
+
lastError = null
|
|
69
|
+
startPing()
|
|
70
|
+
safePost({ type: 'get_status' })
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function scheduleReconnect() {
|
|
74
|
+
if (reconnectTimer) return
|
|
75
|
+
reconnectTimer = setTimeout(() => {
|
|
76
|
+
reconnectTimer = null
|
|
77
|
+
connectToHost()
|
|
78
|
+
}, reconnectDelayMs)
|
|
79
|
+
reconnectDelayMs = Math.min(reconnectDelayMs * 2, 60_000)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function startPing() {
|
|
83
|
+
stopPing()
|
|
84
|
+
pingTimer = setInterval(() => safePost({ type: 'ping' }), PING_INTERVAL_MS)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function stopPing() {
|
|
88
|
+
if (pingTimer) {
|
|
89
|
+
clearInterval(pingTimer)
|
|
90
|
+
pingTimer = null
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function safePost(message) {
|
|
95
|
+
if (!port) return
|
|
96
|
+
try {
|
|
97
|
+
port.postMessage(message)
|
|
98
|
+
} catch (e) {
|
|
99
|
+
lastError = String(e)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function onHostMessage(message) {
|
|
104
|
+
if (!message || typeof message.type !== 'string') return
|
|
105
|
+
switch (message.type) {
|
|
106
|
+
case 'pong':
|
|
107
|
+
case 'status_response':
|
|
108
|
+
break
|
|
109
|
+
case 'mcp_connected':
|
|
110
|
+
mcpClientCount += 1
|
|
111
|
+
break
|
|
112
|
+
case 'mcp_disconnected':
|
|
113
|
+
mcpClientCount = Math.max(0, mcpClientCount - 1)
|
|
114
|
+
break
|
|
115
|
+
case 'tool_request':
|
|
116
|
+
void handleToolRequest(message)
|
|
117
|
+
break
|
|
118
|
+
case 'error':
|
|
119
|
+
lastError = String(message.error || 'error del host')
|
|
120
|
+
break
|
|
121
|
+
default:
|
|
122
|
+
break
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
// Despacho de tools
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
|
|
130
|
+
async function handleToolRequest(message) {
|
|
131
|
+
const params = message.params || {}
|
|
132
|
+
const id = typeof params.id === 'string' ? params.id : null
|
|
133
|
+
const name =
|
|
134
|
+
typeof params.name === 'string' ? params.name : String(message.method || '')
|
|
135
|
+
const args =
|
|
136
|
+
params.arguments && typeof params.arguments === 'object'
|
|
137
|
+
? params.arguments
|
|
138
|
+
: {}
|
|
139
|
+
|
|
140
|
+
if (!id) {
|
|
141
|
+
// Sin id no hay a quién correlacionar la respuesta; no se puede atender.
|
|
142
|
+
return
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
const handler = TOOL_HANDLERS[name]
|
|
147
|
+
if (!handler) {
|
|
148
|
+
respondError(id, `Tool no soportada por la extensión: ${name}`)
|
|
149
|
+
return
|
|
150
|
+
}
|
|
151
|
+
const result = await handler(args)
|
|
152
|
+
safePost({ type: 'tool_response', id, result })
|
|
153
|
+
} catch (e) {
|
|
154
|
+
respondError(id, e && e.message ? e.message : String(e))
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function respondError(id, text) {
|
|
159
|
+
safePost({
|
|
160
|
+
type: 'tool_response',
|
|
161
|
+
id,
|
|
162
|
+
result: { content: [{ type: 'text', text }], isError: true },
|
|
163
|
+
})
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function textResult(text) {
|
|
167
|
+
return { content: [{ type: 'text', text }] }
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function jsonResult(data) {
|
|
171
|
+
return textResult(JSON.stringify(data, null, 2))
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function requireTabId(args) {
|
|
175
|
+
const tabId = args.tabId
|
|
176
|
+
if (typeof tabId !== 'number' || !Number.isInteger(tabId)) {
|
|
177
|
+
throw new Error('Falta el parámetro tabId (número entero).')
|
|
178
|
+
}
|
|
179
|
+
return tabId
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function truncate(text, max) {
|
|
183
|
+
if (typeof text !== 'string') return ''
|
|
184
|
+
if (text.length <= max) return text
|
|
185
|
+
return (
|
|
186
|
+
text.slice(0, max) +
|
|
187
|
+
`\n\n[… texto truncado: ${text.length - max} caracteres omitidos]`
|
|
188
|
+
)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const TOOL_HANDLERS = {
|
|
192
|
+
// Lista pestañas: id, url, title, active.
|
|
193
|
+
async tabs_context_mcp() {
|
|
194
|
+
const tabs = await chrome.tabs.query({})
|
|
195
|
+
const rows = tabs.map(t => ({
|
|
196
|
+
id: t.id,
|
|
197
|
+
url: t.url || '',
|
|
198
|
+
title: t.title || '',
|
|
199
|
+
active: Boolean(t.active),
|
|
200
|
+
windowId: t.windowId,
|
|
201
|
+
}))
|
|
202
|
+
return jsonResult({ tabs: rows })
|
|
203
|
+
},
|
|
204
|
+
|
|
205
|
+
// Abre una pestaña (url opcional) y devuelve su tabId.
|
|
206
|
+
async tabs_create_mcp(args) {
|
|
207
|
+
const createProps = {}
|
|
208
|
+
if (typeof args.url === 'string' && args.url.length > 0) {
|
|
209
|
+
createProps.url = args.url
|
|
210
|
+
}
|
|
211
|
+
const tab = await chrome.tabs.create(createProps)
|
|
212
|
+
return jsonResult({ tabId: tab.id, url: tab.pendingUrl || tab.url || '' })
|
|
213
|
+
},
|
|
214
|
+
|
|
215
|
+
// Navega la pestaña y espera al estado "complete".
|
|
216
|
+
async navigate(args) {
|
|
217
|
+
const tabId = requireTabId(args)
|
|
218
|
+
if (typeof args.url !== 'string' || args.url.length === 0) {
|
|
219
|
+
throw new Error('Falta el parámetro url.')
|
|
220
|
+
}
|
|
221
|
+
await chrome.tabs.update(tabId, { url: args.url })
|
|
222
|
+
await waitForTabComplete(tabId, NAVIGATE_TIMEOUT_MS)
|
|
223
|
+
const tab = await chrome.tabs.get(tabId)
|
|
224
|
+
return jsonResult({
|
|
225
|
+
tabId,
|
|
226
|
+
url: tab.url || '',
|
|
227
|
+
title: tab.title || '',
|
|
228
|
+
status: tab.status || 'complete',
|
|
229
|
+
})
|
|
230
|
+
},
|
|
231
|
+
|
|
232
|
+
// Texto visible del DOM.
|
|
233
|
+
async get_page_text(args) {
|
|
234
|
+
const tabId = requireTabId(args)
|
|
235
|
+
const frames = await chrome.scripting.executeScript({
|
|
236
|
+
target: { tabId },
|
|
237
|
+
func: () => (document.body ? document.body.innerText : ''),
|
|
238
|
+
})
|
|
239
|
+
const text = frames && frames[0] ? String(frames[0].result || '') : ''
|
|
240
|
+
return textResult(truncate(text, MAX_TEXT_CHARS))
|
|
241
|
+
},
|
|
242
|
+
|
|
243
|
+
// Ejecuta JS en el mundo MAIN de la página y devuelve el resultado serializado.
|
|
244
|
+
async javascript_tool(args) {
|
|
245
|
+
const tabId = requireTabId(args)
|
|
246
|
+
if (typeof args.code !== 'string') {
|
|
247
|
+
throw new Error('Falta el parámetro code.')
|
|
248
|
+
}
|
|
249
|
+
const frames = await chrome.scripting.executeScript({
|
|
250
|
+
target: { tabId },
|
|
251
|
+
world: 'MAIN',
|
|
252
|
+
func: code => {
|
|
253
|
+
try {
|
|
254
|
+
// eslint-disable-next-line no-eval
|
|
255
|
+
const value = (0, eval)(code)
|
|
256
|
+
let serialized
|
|
257
|
+
try {
|
|
258
|
+
serialized = JSON.stringify(value)
|
|
259
|
+
} catch (_e) {
|
|
260
|
+
serialized = undefined
|
|
261
|
+
}
|
|
262
|
+
if (serialized === undefined) {
|
|
263
|
+
serialized = String(value)
|
|
264
|
+
}
|
|
265
|
+
return { ok: true, value: serialized }
|
|
266
|
+
} catch (e) {
|
|
267
|
+
return { ok: false, error: e && e.message ? e.message : String(e) }
|
|
268
|
+
}
|
|
269
|
+
},
|
|
270
|
+
args: [args.code],
|
|
271
|
+
})
|
|
272
|
+
const outcome = frames && frames[0] ? frames[0].result : null
|
|
273
|
+
if (!outcome) {
|
|
274
|
+
throw new Error('La ejecución no devolvió resultado (¿pestaña protegida?).')
|
|
275
|
+
}
|
|
276
|
+
if (!outcome.ok) {
|
|
277
|
+
return {
|
|
278
|
+
content: [
|
|
279
|
+
{ type: 'text', text: `Error de ejecución: ${outcome.error}` },
|
|
280
|
+
],
|
|
281
|
+
isError: true,
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return textResult(truncate(String(outcome.value), MAX_TEXT_CHARS))
|
|
285
|
+
},
|
|
286
|
+
|
|
287
|
+
// Busca elementos por texto visible o selector CSS y devuelve refs simples.
|
|
288
|
+
async find(args) {
|
|
289
|
+
const tabId = requireTabId(args)
|
|
290
|
+
if (typeof args.query !== 'string' || args.query.length === 0) {
|
|
291
|
+
throw new Error('Falta el parámetro query.')
|
|
292
|
+
}
|
|
293
|
+
const frames = await chrome.scripting.executeScript({
|
|
294
|
+
target: { tabId },
|
|
295
|
+
func: (query, maxResults) => {
|
|
296
|
+
function cssPath(el) {
|
|
297
|
+
const parts = []
|
|
298
|
+
let node = el
|
|
299
|
+
while (node && node.nodeType === 1 && parts.length < 6) {
|
|
300
|
+
let part = node.tagName.toLowerCase()
|
|
301
|
+
if (node.id) {
|
|
302
|
+
parts.unshift(`${part}#${node.id}`)
|
|
303
|
+
break
|
|
304
|
+
}
|
|
305
|
+
const parent = node.parentElement
|
|
306
|
+
if (parent) {
|
|
307
|
+
const siblings = Array.from(parent.children).filter(
|
|
308
|
+
c => c.tagName === node.tagName,
|
|
309
|
+
)
|
|
310
|
+
if (siblings.length > 1) {
|
|
311
|
+
part += `:nth-of-type(${siblings.indexOf(node) + 1})`
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
parts.unshift(part)
|
|
315
|
+
node = node.parentElement
|
|
316
|
+
}
|
|
317
|
+
return parts.join(' > ')
|
|
318
|
+
}
|
|
319
|
+
function describe(el, ref) {
|
|
320
|
+
const text = (el.innerText || el.value || '')
|
|
321
|
+
.trim()
|
|
322
|
+
.replace(/\s+/g, ' ')
|
|
323
|
+
.slice(0, 120)
|
|
324
|
+
return {
|
|
325
|
+
ref,
|
|
326
|
+
tag: el.tagName.toLowerCase(),
|
|
327
|
+
selector: cssPath(el),
|
|
328
|
+
role: el.getAttribute('role') || null,
|
|
329
|
+
text,
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
const found = []
|
|
333
|
+
// 1) Si la query es un selector CSS válido, úsalo tal cual.
|
|
334
|
+
try {
|
|
335
|
+
const bySelector = document.querySelectorAll(query)
|
|
336
|
+
for (const el of bySelector) {
|
|
337
|
+
if (found.length >= maxResults) break
|
|
338
|
+
found.push(describe(el, `sel-${found.length + 1}`))
|
|
339
|
+
}
|
|
340
|
+
} catch (_e) {
|
|
341
|
+
// No era un selector válido; seguimos con búsqueda por texto.
|
|
342
|
+
}
|
|
343
|
+
// 2) Búsqueda por texto visible en elementos interactivos y comunes.
|
|
344
|
+
if (found.length < maxResults) {
|
|
345
|
+
const lower = query.toLowerCase()
|
|
346
|
+
const candidates = document.querySelectorAll(
|
|
347
|
+
'a, button, input, select, textarea, label, h1, h2, h3, h4, [role], summary, li, td, th, p, span',
|
|
348
|
+
)
|
|
349
|
+
for (const el of candidates) {
|
|
350
|
+
if (found.length >= maxResults) break
|
|
351
|
+
const text = (
|
|
352
|
+
el.innerText ||
|
|
353
|
+
el.value ||
|
|
354
|
+
el.getAttribute('aria-label') ||
|
|
355
|
+
''
|
|
356
|
+
)
|
|
357
|
+
.trim()
|
|
358
|
+
.replace(/\s+/g, ' ')
|
|
359
|
+
if (!text) continue
|
|
360
|
+
if (text.toLowerCase().includes(lower) && text.length < 400) {
|
|
361
|
+
found.push(describe(el, `txt-${found.length + 1}`))
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
return found
|
|
366
|
+
},
|
|
367
|
+
args: [args.query, MAX_FIND_RESULTS],
|
|
368
|
+
})
|
|
369
|
+
const results = frames && frames[0] && frames[0].result ? frames[0].result : []
|
|
370
|
+
return jsonResult({ query: args.query, matches: results })
|
|
371
|
+
},
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Fase 2: computer, read_page, form_input, resize_window,
|
|
375
|
+
// read_console_messages y read_network_requests (definidos en cdp.js).
|
|
376
|
+
Object.assign(TOOL_HANDLERS, CDP_TOOL_HANDLERS)
|
|
377
|
+
|
|
378
|
+
// ---------------------------------------------------------------------------
|
|
379
|
+
// Kuti fase 1 — grabador (captura automática de input)
|
|
380
|
+
// ---------------------------------------------------------------------------
|
|
381
|
+
|
|
382
|
+
// Inyecta el content script recorder.js en todos los frames de la pestaña.
|
|
383
|
+
async function inyectarRecorder(tabId) {
|
|
384
|
+
await chrome.scripting.executeScript({
|
|
385
|
+
target: { tabId, allFrames: true },
|
|
386
|
+
files: ['recorder.js'],
|
|
387
|
+
})
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Empuja un evento del grabador al CLI por el MISMO canal nativo (host →
|
|
391
|
+
// `recorder_event`, sin id de correlación). El host lo reenvía a los clientes
|
|
392
|
+
// MCP; el socketClient del CLI lo reparte a sus oyentes.
|
|
393
|
+
function empujarEventoGrabacion(evento) {
|
|
394
|
+
safePost({ type: 'recorder_event', evento })
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Hook que cdp.js llama al detectar una navegación del frame principal.
|
|
398
|
+
globalThis.kutiEmitirNavegacion = function (tabId, url, capturaBase64) {
|
|
399
|
+
if (!RecorderState.grabando || tabId !== RecorderState.tabGrabada) return
|
|
400
|
+
const evento = { t: 'navigate', url: url || '', tabId, ts: Date.now() }
|
|
401
|
+
if (typeof capturaBase64 === 'string' && capturaBase64.length > 0) {
|
|
402
|
+
evento.captura = capturaBase64
|
|
403
|
+
}
|
|
404
|
+
empujarEventoGrabacion(evento)
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
Object.assign(TOOL_HANDLERS, {
|
|
408
|
+
async recorder_start(args) {
|
|
409
|
+
const tabId = requireTabId(args)
|
|
410
|
+
RecorderState.grabando = true
|
|
411
|
+
RecorderState.tabGrabada = tabId
|
|
412
|
+
RecorderState.capturas = args.capturas === true
|
|
413
|
+
try {
|
|
414
|
+
await inyectarRecorder(tabId)
|
|
415
|
+
} catch (e) {
|
|
416
|
+
RecorderState.grabando = false
|
|
417
|
+
RecorderState.tabGrabada = null
|
|
418
|
+
throw new Error(
|
|
419
|
+
`No se pudo inyectar el capturador en la pestaña ${tabId}: ${e && e.message ? e.message : e}. ` +
|
|
420
|
+
'Las páginas del navegador (chrome://…) y la Web Store no admiten grabación.',
|
|
421
|
+
)
|
|
422
|
+
}
|
|
423
|
+
// Sesión CDP para oír las navegaciones (y capturar por paso si se pidió).
|
|
424
|
+
// Si la pestaña no se puede instrumentar, se graba igual sin el paso
|
|
425
|
+
// `navegar` automático.
|
|
426
|
+
try {
|
|
427
|
+
await asegurarSesion(tabId)
|
|
428
|
+
} catch (_e) {
|
|
429
|
+
// Sin CDP: el content script sigue capturando clics/cambios/teclas.
|
|
430
|
+
}
|
|
431
|
+
return textResult(
|
|
432
|
+
'Grabación iniciada. Opera normalmente en la pestaña: tus clics, cambios de campo y teclas se capturan. ' +
|
|
433
|
+
'Mientras grabas, Chrome muestra la barra «Theus en Chrome ha empezado a depurar este navegador».',
|
|
434
|
+
)
|
|
435
|
+
},
|
|
436
|
+
|
|
437
|
+
async recorder_stop() {
|
|
438
|
+
const tabId = RecorderState.tabGrabada
|
|
439
|
+
RecorderState.grabando = false
|
|
440
|
+
RecorderState.tabGrabada = null
|
|
441
|
+
RecorderState.capturas = false
|
|
442
|
+
if (typeof tabId === 'number') {
|
|
443
|
+
try {
|
|
444
|
+
await chrome.tabs.sendMessage(tabId, { type: 'kuti_recorder_stop' })
|
|
445
|
+
} catch (_e) {
|
|
446
|
+
// El content script pudo desaparecer con una navegación: no pasa nada.
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
return textResult('Grabación detenida.')
|
|
450
|
+
},
|
|
451
|
+
})
|
|
452
|
+
|
|
453
|
+
function waitForTabComplete(tabId, timeoutMs) {
|
|
454
|
+
return new Promise((resolve, reject) => {
|
|
455
|
+
let settled = false
|
|
456
|
+
const timer = setTimeout(() => {
|
|
457
|
+
if (settled) return
|
|
458
|
+
settled = true
|
|
459
|
+
chrome.tabs.onUpdated.removeListener(listener)
|
|
460
|
+
reject(new Error(`La página no terminó de cargar en ${Math.round(timeoutMs / 1000)} s.`))
|
|
461
|
+
}, timeoutMs)
|
|
462
|
+
function listener(updatedTabId, changeInfo) {
|
|
463
|
+
if (updatedTabId === tabId && changeInfo.status === 'complete') {
|
|
464
|
+
if (settled) return
|
|
465
|
+
settled = true
|
|
466
|
+
clearTimeout(timer)
|
|
467
|
+
chrome.tabs.onUpdated.removeListener(listener)
|
|
468
|
+
resolve()
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
chrome.tabs.onUpdated.addListener(listener)
|
|
472
|
+
// Por si la pestaña ya estaba completa cuando llegamos.
|
|
473
|
+
chrome.tabs.get(tabId).then(tab => {
|
|
474
|
+
if (tab.status === 'complete' && !settled) {
|
|
475
|
+
settled = true
|
|
476
|
+
clearTimeout(timer)
|
|
477
|
+
chrome.tabs.onUpdated.removeListener(listener)
|
|
478
|
+
resolve()
|
|
479
|
+
}
|
|
480
|
+
}, () => {})
|
|
481
|
+
})
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// ---------------------------------------------------------------------------
|
|
485
|
+
// Estado para el popup
|
|
486
|
+
// ---------------------------------------------------------------------------
|
|
487
|
+
|
|
488
|
+
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|
489
|
+
if (message && message.type === 'get_state') {
|
|
490
|
+
sendResponse({
|
|
491
|
+
hostConnected: Boolean(port),
|
|
492
|
+
cliConnected: mcpClientCount > 0,
|
|
493
|
+
cliClients: mcpClientCount,
|
|
494
|
+
recording: RecorderState.grabando,
|
|
495
|
+
lastError,
|
|
496
|
+
})
|
|
497
|
+
return false
|
|
498
|
+
}
|
|
499
|
+
// Evento del grabador (content script recorder.js). Solo se reenvía si la
|
|
500
|
+
// grabación está activa y viene de la pestaña que grabamos.
|
|
501
|
+
if (message && message.type === 'kuti_recorder_event') {
|
|
502
|
+
if (
|
|
503
|
+
RecorderState.grabando &&
|
|
504
|
+
sender &&
|
|
505
|
+
sender.tab &&
|
|
506
|
+
sender.tab.id === RecorderState.tabGrabada &&
|
|
507
|
+
message.evento &&
|
|
508
|
+
typeof message.evento === 'object'
|
|
509
|
+
) {
|
|
510
|
+
const evento = {
|
|
511
|
+
...message.evento,
|
|
512
|
+
tabId: sender.tab.id,
|
|
513
|
+
url: sender.url || (sender.tab && sender.tab.url) || '',
|
|
514
|
+
ts: Date.now(),
|
|
515
|
+
}
|
|
516
|
+
empujarEventoGrabacion(evento)
|
|
517
|
+
}
|
|
518
|
+
return false
|
|
519
|
+
}
|
|
520
|
+
return false
|
|
521
|
+
})
|
|
522
|
+
|
|
523
|
+
// Reinyecta el capturador tras cada navegación de la pestaña grabada: el
|
|
524
|
+
// content script muere al cambiar de página.
|
|
525
|
+
chrome.tabs.onUpdated.addListener((tabId, changeInfo) => {
|
|
526
|
+
if (
|
|
527
|
+
RecorderState.grabando &&
|
|
528
|
+
tabId === RecorderState.tabGrabada &&
|
|
529
|
+
changeInfo.status === 'complete'
|
|
530
|
+
) {
|
|
531
|
+
inyectarRecorder(tabId).catch(() => {
|
|
532
|
+
// Página protegida o carrera con un cierre: se ignora.
|
|
533
|
+
})
|
|
534
|
+
}
|
|
535
|
+
})
|
|
536
|
+
|
|
537
|
+
// Arranque: conectar al host (también al instalarse o reiniciar el navegador).
|
|
538
|
+
chrome.runtime.onStartup.addListener(connectToHost)
|
|
539
|
+
chrome.runtime.onInstalled.addListener(connectToHost)
|
|
540
|
+
connectToHost()
|