skykoi 2026.3.85 → 2026.3.87

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.
Files changed (89) hide show
  1. package/assets/chrome-extension/background.js +317 -206
  2. package/assets/chrome-extension/manifest.json +4 -4
  3. package/dist/{acp-cli-Dgvy6bKG.js → acp-cli-Cue9eQD1.js} +3 -3
  4. package/dist/{agent-B33p8GO3.js → agent-7qp7NaRz.js} +7 -7
  5. package/dist/{archive-09rsl6Ll.js → archive-C91mkBc-.js} +1 -1
  6. package/dist/{audit-Bk2eeCaJ.js → audit-2oghn2vt.js} +5 -5
  7. package/dist/{auth-health-zenw66eK.js → auth-health-utpbB5vt.js} +1 -1
  8. package/dist/build-info.json +3 -3
  9. package/dist/{call-CWNr3uUO.js → call-CB8hoic2.js} +3 -8
  10. package/dist/canvas-host/a2ui/.bundle.hash +1 -1
  11. package/dist/{channel-options-CclY3Kao.js → channel-options-wuxVoUIe.js} +2 -2
  12. package/dist/{channel-summary-Ae-IsE8h.js → channel-summary-CfxC-1nZ.js} +4 -4
  13. package/dist/{channels-cli-BkHQm13G.js → channels-cli-DAF3ekZo.js} +23 -23
  14. package/dist/{chrome-BXUOz50D.js → chrome-CKj-JW7a.js} +1 -2
  15. package/dist/cli/daemon-cli.js +1 -1
  16. package/dist/{cli-C0FJD3jr.js → cli-DWiC-T7N.js} +17 -17
  17. package/dist/{completion-cli-CBuTZYSt.js → completion-cli-BXlPFqTC.js} +1 -1
  18. package/dist/{config-DKY_dinf.js → config-CtAp-ADe.js} +1 -1
  19. package/dist/{config-guard-DEz1BuaV.js → config-guard-DZZyYL4S.js} +20 -20
  20. package/dist/{configure-DDNEcXdF.js → configure-BdS5VyTl.js} +7 -7
  21. package/dist/{control-service-IfyiVJzo.js → control-service-BbXQeQwf.js} +4 -4
  22. package/dist/{cron-cli--_8V5Gf3.js → cron-cli-B39VyfFH.js} +4 -4
  23. package/dist/{daemon-cli-B9O89I4b.js → daemon-cli-CpcZdCm0.js} +8 -8
  24. package/dist/{daemon-runtime-BlJMOmTd.js → daemon-runtime-CUIuNwaa.js} +1 -1
  25. package/dist/{deliver-MpXBgM3h.js → deliver-CQeQBobe.js} +11 -11
  26. package/dist/{deps-DBDWSLXZ.js → deps-BjTw2X80.js} +2 -2
  27. package/dist/{devices-cli-DSK6HYcw.js → devices-cli-AHcYvI4G.js} +3 -3
  28. package/dist/{directory-cli-uUVaHSGb.js → directory-cli-Ceuljq1g.js} +2 -2
  29. package/dist/{dispatcher-SOgLWHV0.js → dispatcher-D-yTQPvS.js} +1 -1
  30. package/dist/{dns-cli-BQYu44-7.js → dns-cli-GL7D9rXf.js} +2 -2
  31. package/dist/{doctor-2S0oU0gS.js → doctor-DhRjIF9C.js} +13 -13
  32. package/dist/entry.js +1 -1
  33. package/dist/{exec-approvals-cli-BjXeuNZX.js → exec-approvals-cli-DAlM2LHw.js} +5 -5
  34. package/dist/extensionAPI.js +17 -17
  35. package/dist/{gateway-cli-I9an3dZ_.js → gateway-cli-CWu5Ho2O.js} +146 -78
  36. package/dist/{gateway-rpc-CLWNaD7x.js → gateway-rpc-B2QEauAF.js} +1 -1
  37. package/dist/{github-copilot-auth-vZ5-Nn7c.js → github-copilot-auth-vHH_raqP.js} +4 -4
  38. package/dist/{health-format-CJjcHVlY.js → health-format-CzWNMbcr.js} +6 -6
  39. package/dist/{hooks-cli-DpD_F4PN.js → hooks-cli-BTNJ3TcY.js} +19 -19
  40. package/dist/{image-CwUNm3Vl.js → image-C1XBkrja.js} +3 -3
  41. package/dist/index.js +43 -43
  42. package/dist/{installs-DzWk8S7n.js → installs-BlumYhIK.js} +1 -1
  43. package/dist/{login-qr-BL0IdPTR.js → login-qr-C1VpjiOL.js} +1 -1
  44. package/dist/{logs-cli-DhMg4ByY.js → logs-cli-DzVqMDvp.js} +4 -4
  45. package/dist/{manager-CSZdstQ8.js → manager-BYL4_VQc.js} +1 -1
  46. package/dist/{model-selection-CkeQWhia.js → model-selection-BOiaAkb5.js} +117 -2
  47. package/dist/{models-cli-cQKCcCPL.js → models-cli-DzDVIpeN.js} +19 -19
  48. package/dist/{node-cli-DnLm5atJ.js → node-cli-DkC6wrL-.js} +9 -9
  49. package/dist/{nodes-cli-ka_n7rhe.js → nodes-cli-BlOW7-46.js} +4 -4
  50. package/dist/{onboard-channels-BhNBrV_Q.js → onboard-channels-XZYp7zlA.js} +3 -3
  51. package/dist/{onboard-skills-BYBSWtSD.js → onboard-skills-C1A_Lqwu.js} +5 -5
  52. package/dist/{onboarding-CfL5vV28.js → onboarding-DBKDeCdL.js} +23 -25
  53. package/dist/{pairing-cli-B5vocCzh.js → pairing-cli-DpnNmz7M.js} +2 -2
  54. package/dist/{pi-embedded-helpers-Cu5s400O.js → pi-embedded-helpers-BPKQ1WbA.js} +2 -2
  55. package/dist/{pi-tools.policy-BZg7tJcw.js → pi-tools.policy-DRWwTCPx.js} +1 -1
  56. package/dist/{plugin-auto-enable-DQnhAmyB.js → plugin-auto-enable-CJPNyFbz.js} +1 -1
  57. package/dist/{plugin-registry-zFUHKwEt.js → plugin-registry-k7Abn6gN.js} +2 -2
  58. package/dist/plugin-sdk/browser/constants.d.ts +0 -9
  59. package/dist/plugin-sdk/gateway/call.d.ts +0 -5
  60. package/dist/plugin-sdk/index.js +127 -15
  61. package/dist/plugin-sdk/media/input-files.d.ts +1 -1
  62. package/dist/{plugins-cli-BldSwhzI.js → plugins-cli-OM3taC51.js} +21 -21
  63. package/dist/{ports-CrDpWbK_.js → ports-_xuvA1tL.js} +1 -1
  64. package/dist/{program-CBdq2IEp.js → program-BLBYWrqo.js} +6 -6
  65. package/dist/{pw-ai-D8a23rhS.js → pw-ai-DeetJxIQ.js} +1 -1
  66. package/dist/{register.subclis-B2TAYlxQ.js → register.subclis-CCxtEfCG.js} +27 -27
  67. package/dist/{reply-CogA-BUj.js → reply-3b9cJ0zw.js} +25 -25
  68. package/dist/{routes-DwesuQto.js → routes-C3SV3r0e.js} +6 -6
  69. package/dist/{rpc-DyvRsbXd.js → rpc-CgIk2J7j.js} +1 -1
  70. package/dist/{run-main-B7Dw2tsC.js → run-main-B-FVRwsN.js} +44 -44
  71. package/dist/{runner-DEGFhun2.js → runner-DAvW5RKS.js} +5 -5
  72. package/dist/{sandbox-hEnK_s1a.js → sandbox-Dwo6SOe8.js} +4 -4
  73. package/dist/{sandbox-cli-8ZGWxwkF.js → sandbox-cli-CzUmMGCu.js} +6 -6
  74. package/dist/{security-cli-C_qpjrTc.js → security-cli-Bli2MaXl.js} +9 -9
  75. package/dist/{server-context-DWchuAqq.js → server-context-Mxz82xGQ.js} +4 -5
  76. package/dist/{server-node-events-DDSSdjZd.js → server-node-events-BywQiZ0z.js} +19 -19
  77. package/dist/{service-audit-B-95c3u8.js → service-audit-DqDVkfEJ.js} +1 -1
  78. package/dist/{skills-cli-SZCYw7cN.js → skills-cli-DnMWBmAF.js} +2 -2
  79. package/dist/{status-JDhaBIhA.js → status-DuFP6Ftr.js} +2 -2
  80. package/dist/{system-cli-CEvYC8RJ.js → system-cli-Bu41yZ2t.js} +4 -4
  81. package/dist/{tui-Bo9ufl9J.js → tui-D-P5u7lH.js} +4 -4
  82. package/dist/{tui-cli-B2eE3wj1.js → tui-cli-BPdJ1VNP.js} +10 -10
  83. package/dist/{update-7hnqqWjP.js → update-Cfz1LOGa.js} +1 -1
  84. package/dist/{update-cli-BIYn-QeV.js → update-cli-DvAQb1FT.js} +31 -31
  85. package/dist/{update-runner-B2RC_Kcz.js → update-runner-BCnc1i3P.js} +5 -5
  86. package/dist/{webhooks-cli-D-BRmxqt.js → webhooks-cli-S_jSOfkR.js} +2 -2
  87. package/package.json +1 -1
  88. package/assets/chrome-extension/offscreen.html +0 -4
  89. package/assets/chrome-extension/offscreen.js +0 -107
@@ -1,89 +1,178 @@
1
- /**
2
- * SKYKOI Browser Relay — Service Worker (MV3)
3
- *
4
- * Handles chrome.debugger API calls. WebSocket to the relay server is maintained
5
- * by an offscreen document (offscreen.js) for persistent connectivity.
6
- */
1
+ const DEFAULT_PORT = 18792
7
2
 
8
3
  const BADGE = {
9
4
  on: { text: 'ON', color: '#FF5A36' },
10
5
  off: { text: '', color: '#000000' },
11
- connecting: { text: '...', color: '#F59E0B' },
6
+ connecting: { text: '', color: '#F59E0B' },
12
7
  error: { text: '!', color: '#B91C1C' },
13
8
  }
14
9
 
15
- let relayConnected = false
16
- let offscreenPort = null
10
+ /** @type {WebSocket|null} */
11
+ let relayWs = null
12
+ /** @type {Promise<void>|null} */
13
+ let relayConnectPromise = null
14
+
17
15
  let debuggerListenersInstalled = false
16
+
18
17
  let nextSession = 1
19
18
 
20
- /** @type {Map<number, {state:'connecting'|'connected', sessionId?:string, targetId?:string}>} */
19
+ /** @type {Map<number, {state:'connecting'|'connected', sessionId?:string, targetId?:string, attachOrder?:number}>} */
21
20
  const tabs = new Map()
22
21
  /** @type {Map<string, number>} */
23
22
  const tabBySession = new Map()
24
23
  /** @type {Map<string, number>} */
25
24
  const childSessionToTab = new Map()
25
+
26
26
  /** @type {Map<number, {resolve:(v:any)=>void, reject:(e:Error)=>void}>} */
27
27
  const pending = new Map()
28
28
 
29
- // ---------------------------------------------------------------------------
30
- // Offscreen document lifecycle
31
- // ---------------------------------------------------------------------------
29
+ function nowStack() {
30
+ try {
31
+ return new Error().stack || ''
32
+ } catch {
33
+ return ''
34
+ }
35
+ }
32
36
 
33
- async function ensureOffscreen() {
34
- const contexts = await chrome.runtime.getContexts({
35
- contextTypes: ['OFFSCREEN_DOCUMENT'],
36
- }).catch(() => [])
37
- if (contexts.length > 0) return
37
+ async function getRelayPort() {
38
+ const stored = await chrome.storage.local.get(['relayPort'])
39
+ const raw = stored.relayPort
40
+ const n = Number.parseInt(String(raw || ''), 10)
41
+ if (!Number.isFinite(n) || n <= 0 || n > 65535) return DEFAULT_PORT
42
+ return n
43
+ }
38
44
 
39
- await chrome.offscreen.createDocument({
40
- url: 'offscreen.html',
41
- reasons: [chrome.offscreen.Reason.BLOBS],
42
- justification: 'Persistent WebSocket to SKYKOI relay server',
43
- })
45
+ function setBadge(tabId, kind) {
46
+ const cfg = BADGE[kind]
47
+ void chrome.action.setBadgeText({ tabId, text: cfg.text })
48
+ void chrome.action.setBadgeBackgroundColor({ tabId, color: cfg.color })
49
+ void chrome.action.setBadgeTextColor({ tabId, color: '#FFFFFF' }).catch(() => {})
44
50
  }
45
51
 
46
- function ensureOffscreenPort() {
47
- if (offscreenPort) return
48
- offscreenPort = chrome.runtime.connect({ name: 'skykoi-relay' })
49
- offscreenPort.onMessage.addListener(onOffscreenMessage)
50
- offscreenPort.onDisconnect.addListener(() => {
51
- offscreenPort = null
52
- relayConnected = false
53
- onRelayClosed('port-disconnected')
54
- // Recreate after brief delay
55
- setTimeout(() => { void ensureOffscreen().then(ensureOffscreenPort) }, 2000)
56
- })
52
+ async function ensureRelayConnection() {
53
+ if (relayWs && relayWs.readyState === WebSocket.OPEN) return
54
+ if (relayConnectPromise) return await relayConnectPromise
55
+
56
+ relayConnectPromise = (async () => {
57
+ const port = await getRelayPort()
58
+ const httpBase = `http://127.0.0.1:${port}`
59
+ const wsUrl = `ws://127.0.0.1:${port}/extension`
60
+
61
+ // Fast preflight: is the relay server up?
62
+ try {
63
+ await fetch(`${httpBase}/`, { method: 'HEAD', signal: AbortSignal.timeout(2000) })
64
+ } catch (err) {
65
+ throw new Error(`Relay server not reachable at ${httpBase} (${String(err)})`)
66
+ }
67
+
68
+ const ws = new WebSocket(wsUrl)
69
+ relayWs = ws
70
+
71
+ await new Promise((resolve, reject) => {
72
+ const t = setTimeout(() => reject(new Error('WebSocket connect timeout')), 5000)
73
+ ws.onopen = () => {
74
+ clearTimeout(t)
75
+ resolve()
76
+ }
77
+ ws.onerror = () => {
78
+ clearTimeout(t)
79
+ reject(new Error('WebSocket connect failed'))
80
+ }
81
+ ws.onclose = (ev) => {
82
+ clearTimeout(t)
83
+ reject(new Error(`WebSocket closed (${ev.code} ${ev.reason || 'no reason'})`))
84
+ }
85
+ })
86
+
87
+ ws.onmessage = (event) => void onRelayMessage(String(event.data || ''))
88
+ ws.onclose = () => onRelayClosed('closed')
89
+ ws.onerror = () => onRelayClosed('error')
90
+
91
+ if (!debuggerListenersInstalled) {
92
+ debuggerListenersInstalled = true
93
+ chrome.debugger.onEvent.addListener(onDebuggerEvent)
94
+ chrome.debugger.onDetach.addListener(onDebuggerDetach)
95
+ }
96
+ })()
97
+
98
+ try {
99
+ await relayConnectPromise
100
+ } finally {
101
+ relayConnectPromise = null
102
+ }
57
103
  }
58
104
 
59
- function sendToRelay(payload) {
60
- if (!offscreenPort || !relayConnected) throw new Error('Relay not connected')
61
- offscreenPort.postMessage(payload)
105
+ function onRelayClosed(reason) {
106
+ relayWs = null
107
+ for (const [id, p] of pending.entries()) {
108
+ pending.delete(id)
109
+ p.reject(new Error(`Relay disconnected (${reason})`))
110
+ }
111
+
112
+ for (const tabId of tabs.keys()) {
113
+ void chrome.debugger.detach({ tabId }).catch(() => {})
114
+ setBadge(tabId, 'connecting')
115
+ void chrome.action.setTitle({
116
+ tabId,
117
+ title: 'SKYKOI Browser Relay: disconnected (click to re-attach)',
118
+ })
119
+ }
120
+ tabs.clear()
121
+ tabBySession.clear()
122
+ childSessionToTab.clear()
62
123
  }
63
124
 
64
- // ---------------------------------------------------------------------------
65
- // Messages from offscreen document
66
- // ---------------------------------------------------------------------------
125
+ function sendToRelay(payload) {
126
+ const ws = relayWs
127
+ if (!ws || ws.readyState !== WebSocket.OPEN) {
128
+ throw new Error('Relay not connected')
129
+ }
130
+ ws.send(JSON.stringify(payload))
131
+ }
67
132
 
68
- function onOffscreenMessage(msg) {
69
- if (!msg) return
133
+ async function maybeOpenHelpOnce() {
134
+ try {
135
+ const stored = await chrome.storage.local.get(['helpOnErrorShown'])
136
+ if (stored.helpOnErrorShown === true) return
137
+ await chrome.storage.local.set({ helpOnErrorShown: true })
138
+ await chrome.runtime.openOptionsPage()
139
+ } catch {
140
+ // ignore
141
+ }
142
+ }
70
143
 
71
- // Relay status updates
72
- if (msg.type === 'relay-status') {
73
- const wasConnected = relayConnected
74
- relayConnected = msg.connected
75
- if (msg.connected && !wasConnected) {
76
- console.log('[bg] Relay connected via offscreen')
77
- void autoAttachAllTabs()
144
+ function requestFromRelay(command) {
145
+ const id = command.id
146
+ return new Promise((resolve, reject) => {
147
+ pending.set(id, { resolve, reject })
148
+ try {
149
+ sendToRelay(command)
150
+ } catch (err) {
151
+ pending.delete(id)
152
+ reject(err instanceof Error ? err : new Error(String(err)))
78
153
  }
79
- if (!msg.connected && wasConnected) {
80
- onRelayClosed('relay-disconnected')
154
+ })
155
+ }
156
+
157
+ async function onRelayMessage(text) {
158
+ /** @type {any} */
159
+ let msg
160
+ try {
161
+ msg = JSON.parse(text)
162
+ } catch {
163
+ return
164
+ }
165
+
166
+ if (msg && msg.method === 'ping') {
167
+ try {
168
+ sendToRelay({ method: 'pong' })
169
+ } catch {
170
+ // ignore
81
171
  }
82
172
  return
83
173
  }
84
174
 
85
- // Response to a pending CDP command
86
- if (typeof msg.id === 'number' && (msg.result !== undefined || msg.error !== undefined)) {
175
+ if (msg && typeof msg.id === 'number' && (msg.result !== undefined || msg.error !== undefined)) {
87
176
  const p = pending.get(msg.id)
88
177
  if (!p) return
89
178
  pending.delete(msg.id)
@@ -92,50 +181,32 @@ function onOffscreenMessage(msg) {
92
181
  return
93
182
  }
94
183
 
95
- // CDP command forwarded from relay
96
- if (typeof msg.id === 'number' && msg.method === 'forwardCDPCommand') {
97
- void (async () => {
98
- try {
99
- const result = await handleForwardCdpCommand(msg)
100
- sendToRelay({ id: msg.id, result })
101
- } catch (err) {
102
- sendToRelay({ id: msg.id, error: err instanceof Error ? err.message : String(err) })
103
- }
104
- })()
184
+ if (msg && typeof msg.id === 'number' && msg.method === 'forwardCDPCommand') {
185
+ try {
186
+ const result = await handleForwardCdpCommand(msg)
187
+ sendToRelay({ id: msg.id, result })
188
+ } catch (err) {
189
+ sendToRelay({ id: msg.id, error: err instanceof Error ? err.message : String(err) })
190
+ }
105
191
  }
106
192
  }
107
193
 
108
- // ---------------------------------------------------------------------------
109
- // Badge & relay disconnect
110
- // ---------------------------------------------------------------------------
111
-
112
- function setBadge(tabId, kind) {
113
- const cfg = BADGE[kind]
114
- void chrome.action.setBadgeText({ tabId, text: cfg.text })
115
- void chrome.action.setBadgeBackgroundColor({ tabId, color: cfg.color })
116
- void chrome.action.setBadgeTextColor({ tabId, color: '#FFFFFF' }).catch(() => {})
194
+ function getTabBySessionId(sessionId) {
195
+ const direct = tabBySession.get(sessionId)
196
+ if (direct) return { tabId: direct, kind: 'main' }
197
+ const child = childSessionToTab.get(sessionId)
198
+ if (child) return { tabId: child, kind: 'child' }
199
+ return null
117
200
  }
118
201
 
119
- function onRelayClosed(reason) {
120
- for (const [id, p] of pending.entries()) {
121
- pending.delete(id)
122
- p.reject(new Error(`Relay disconnected (${reason})`))
202
+ function getTabByTargetId(targetId) {
203
+ for (const [tabId, tab] of tabs.entries()) {
204
+ if (tab.targetId === targetId) return tabId
123
205
  }
124
- for (const tabId of tabs.keys()) {
125
- void chrome.debugger.detach({ tabId }).catch(() => {})
126
- setBadge(tabId, 'off')
127
- }
128
- tabs.clear()
129
- tabBySession.clear()
130
- childSessionToTab.clear()
206
+ return null
131
207
  }
132
208
 
133
- // ---------------------------------------------------------------------------
134
- // Tab attach / detach
135
- // ---------------------------------------------------------------------------
136
-
137
- async function attachTab(tabId) {
138
- if (tabs.has(tabId)) return
209
+ async function attachTab(tabId, opts = {}) {
139
210
  const debuggee = { tabId }
140
211
  await chrome.debugger.attach(debuggee, '1.3')
141
212
  await chrome.debugger.sendCommand(debuggee, 'Page.enable').catch(() => {})
@@ -143,21 +214,36 @@ async function attachTab(tabId) {
143
214
  const info = /** @type {any} */ (await chrome.debugger.sendCommand(debuggee, 'Target.getTargetInfo'))
144
215
  const targetInfo = info?.targetInfo
145
216
  const targetId = String(targetInfo?.targetId || '').trim()
146
- if (!targetId) throw new Error('No targetId')
217
+ if (!targetId) {
218
+ throw new Error('Target.getTargetInfo returned no targetId')
219
+ }
147
220
 
148
221
  const sessionId = `cb-tab-${nextSession++}`
149
- tabs.set(tabId, { state: 'connected', sessionId, targetId })
150
- tabBySession.set(sessionId, tabId)
222
+ const attachOrder = nextSession
151
223
 
152
- sendToRelay({
153
- method: 'forwardCDPEvent',
154
- params: {
155
- method: 'Target.attachedToTarget',
156
- params: { sessionId, targetInfo: { ...targetInfo, attached: true }, waitingForDebugger: false },
157
- },
224
+ tabs.set(tabId, { state: 'connected', sessionId, targetId, attachOrder })
225
+ tabBySession.set(sessionId, tabId)
226
+ void chrome.action.setTitle({
227
+ tabId,
228
+ title: 'SKYKOI Browser Relay: attached (click to detach)',
158
229
  })
230
+
231
+ if (!opts.skipAttachedEvent) {
232
+ sendToRelay({
233
+ method: 'forwardCDPEvent',
234
+ params: {
235
+ method: 'Target.attachedToTarget',
236
+ params: {
237
+ sessionId,
238
+ targetInfo: { ...targetInfo, attached: true },
239
+ waitingForDebugger: false,
240
+ },
241
+ },
242
+ })
243
+ }
244
+
159
245
  setBadge(tabId, 'on')
160
- void chrome.action.setTitle({ tabId, title: 'SKYKOI: attached' })
246
+ return { sessionId, targetId }
161
247
  }
162
248
 
163
249
  async function detachTab(tabId, reason) {
@@ -166,30 +252,69 @@ async function detachTab(tabId, reason) {
166
252
  try {
167
253
  sendToRelay({
168
254
  method: 'forwardCDPEvent',
169
- params: { method: 'Target.detachedFromTarget', params: { sessionId: tab.sessionId, targetId: tab.targetId, reason } },
255
+ params: {
256
+ method: 'Target.detachedFromTarget',
257
+ params: { sessionId: tab.sessionId, targetId: tab.targetId, reason },
258
+ },
170
259
  })
171
- } catch { /* ignore */ }
260
+ } catch {
261
+ // ignore
262
+ }
172
263
  }
264
+
173
265
  if (tab?.sessionId) tabBySession.delete(tab.sessionId)
174
266
  tabs.delete(tabId)
175
- for (const [s, t] of childSessionToTab.entries()) { if (t === tabId) childSessionToTab.delete(s) }
176
- void chrome.debugger.detach({ tabId }).catch(() => {})
267
+
268
+ for (const [childSessionId, parentTabId] of childSessionToTab.entries()) {
269
+ if (parentTabId === tabId) childSessionToTab.delete(childSessionId)
270
+ }
271
+
272
+ try {
273
+ await chrome.debugger.detach({ tabId })
274
+ } catch {
275
+ // ignore
276
+ }
277
+
177
278
  setBadge(tabId, 'off')
178
- void chrome.action.setTitle({ tabId, title: 'SKYKOI Browser Relay' })
279
+ void chrome.action.setTitle({
280
+ tabId,
281
+ title: 'SKYKOI Browser Relay (click to attach/detach)',
282
+ })
179
283
  }
180
284
 
181
- // ---------------------------------------------------------------------------
182
- // CDP command handling (from relay via offscreen)
183
- // ---------------------------------------------------------------------------
285
+ async function connectOrToggleForActiveTab() {
286
+ const [active] = await chrome.tabs.query({ active: true, currentWindow: true })
287
+ const tabId = active?.id
288
+ if (!tabId) return
184
289
 
185
- function getTabBySessionId(sid) {
186
- const d = tabBySession.get(sid); if (d) return { tabId: d }
187
- const c = childSessionToTab.get(sid); if (c) return { tabId: c }
188
- return null
189
- }
190
- function getTabByTargetId(tid) {
191
- for (const [id, t] of tabs.entries()) { if (t.targetId === tid) return id }
192
- return null
290
+ const existing = tabs.get(tabId)
291
+ if (existing?.state === 'connected') {
292
+ await detachTab(tabId, 'toggle')
293
+ return
294
+ }
295
+
296
+ tabs.set(tabId, { state: 'connecting' })
297
+ setBadge(tabId, 'connecting')
298
+ void chrome.action.setTitle({
299
+ tabId,
300
+ title: 'SKYKOI Browser Relay: connecting to local relay…',
301
+ })
302
+
303
+ try {
304
+ await ensureRelayConnection()
305
+ await attachTab(tabId)
306
+ } catch (err) {
307
+ tabs.delete(tabId)
308
+ setBadge(tabId, 'error')
309
+ void chrome.action.setTitle({
310
+ tabId,
311
+ title: 'SKYKOI Browser Relay: relay not running (open options for setup)',
312
+ })
313
+ void maybeOpenHelpOnce()
314
+ // Extra breadcrumbs in chrome://extensions service worker logs.
315
+ const message = err instanceof Error ? err.message : String(err)
316
+ console.warn('attach failed', message, nowStack())
317
+ }
193
318
  }
194
319
 
195
320
  async function handleForwardCdpCommand(msg) {
@@ -197,131 +322,117 @@ async function handleForwardCdpCommand(msg) {
197
322
  const params = msg?.params?.params || undefined
198
323
  const sessionId = typeof msg?.params?.sessionId === 'string' ? msg.params.sessionId : undefined
199
324
 
325
+ // Map command to tab
200
326
  const bySession = sessionId ? getTabBySessionId(sessionId) : null
201
327
  const targetId = typeof params?.targetId === 'string' ? params.targetId : undefined
202
- const tabId = bySession?.tabId
203
- || (targetId ? getTabByTargetId(targetId) : null)
204
- || (() => { for (const [id, t] of tabs.entries()) { if (t.state === 'connected') return id } return null })()
205
- if (!tabId) throw new Error(`No attached tab for ${method}`)
328
+ const tabId =
329
+ bySession?.tabId ||
330
+ (targetId ? getTabByTargetId(targetId) : null) ||
331
+ (() => {
332
+ // No sessionId: pick the first connected tab (stable-ish).
333
+ for (const [id, tab] of tabs.entries()) {
334
+ if (tab.state === 'connected') return id
335
+ }
336
+ return null
337
+ })()
206
338
 
339
+ if (!tabId) throw new Error(`No attached tab for method ${method}`)
340
+
341
+ /** @type {chrome.debugger.DebuggerSession} */
207
342
  const debuggee = { tabId }
208
343
 
209
344
  if (method === 'Runtime.enable') {
210
- await chrome.debugger.sendCommand(debuggee, 'Runtime.disable').catch(() => {})
211
- await new Promise(r => setTimeout(r, 50))
345
+ try {
346
+ await chrome.debugger.sendCommand(debuggee, 'Runtime.disable')
347
+ await new Promise((r) => setTimeout(r, 50))
348
+ } catch {
349
+ // ignore
350
+ }
212
351
  return await chrome.debugger.sendCommand(debuggee, 'Runtime.enable', params)
213
352
  }
353
+
214
354
  if (method === 'Target.createTarget') {
215
355
  const url = typeof params?.url === 'string' ? params.url : 'about:blank'
216
356
  const tab = await chrome.tabs.create({ url, active: false })
217
357
  if (!tab.id) throw new Error('Failed to create tab')
218
- await new Promise(r => setTimeout(r, 100))
219
- await attachTab(tab.id)
220
- const t = tabs.get(tab.id)
221
- return { targetId: t?.targetId }
358
+ await new Promise((r) => setTimeout(r, 100))
359
+ const attached = await attachTab(tab.id)
360
+ return { targetId: attached.targetId }
222
361
  }
362
+
223
363
  if (method === 'Target.closeTarget') {
224
- const tid = typeof params?.targetId === 'string' ? params.targetId : ''
225
- const toClose = tid ? getTabByTargetId(tid) : tabId
364
+ const target = typeof params?.targetId === 'string' ? params.targetId : ''
365
+ const toClose = target ? getTabByTargetId(target) : tabId
226
366
  if (!toClose) return { success: false }
227
- await chrome.tabs.remove(toClose).catch(() => {})
367
+ try {
368
+ await chrome.tabs.remove(toClose)
369
+ } catch {
370
+ return { success: false }
371
+ }
228
372
  return { success: true }
229
373
  }
374
+
230
375
  if (method === 'Target.activateTarget') {
231
- const tid = typeof params?.targetId === 'string' ? params.targetId : ''
232
- const toFocus = tid ? getTabByTargetId(tid) : tabId
233
- if (toFocus) {
234
- const tab = await chrome.tabs.get(toFocus).catch(() => null)
235
- if (tab?.windowId) await chrome.windows.update(tab.windowId, { focused: true }).catch(() => {})
236
- await chrome.tabs.update(toFocus, { active: true }).catch(() => {})
376
+ const target = typeof params?.targetId === 'string' ? params.targetId : ''
377
+ const toActivate = target ? getTabByTargetId(target) : tabId
378
+ if (!toActivate) return {}
379
+ const tab = await chrome.tabs.get(toActivate).catch(() => null)
380
+ if (!tab) return {}
381
+ if (tab.windowId) {
382
+ await chrome.windows.update(tab.windowId, { focused: true }).catch(() => {})
237
383
  }
384
+ await chrome.tabs.update(toActivate, { active: true }).catch(() => {})
238
385
  return {}
239
386
  }
240
387
 
241
388
  const tabState = tabs.get(tabId)
242
- const mainSid = tabState?.sessionId
243
- const debuggerSession = (sessionId && mainSid && sessionId !== mainSid) ? { ...debuggee, sessionId } : debuggee
389
+ const mainSessionId = tabState?.sessionId
390
+ const debuggerSession =
391
+ sessionId && mainSessionId && sessionId !== mainSessionId
392
+ ? { ...debuggee, sessionId }
393
+ : debuggee
394
+
244
395
  return await chrome.debugger.sendCommand(debuggerSession, method, params)
245
396
  }
246
397
 
247
- // ---------------------------------------------------------------------------
248
- // Debugger event forwarding
249
- // ---------------------------------------------------------------------------
250
-
251
- function installDebuggerListeners() {
252
- if (debuggerListenersInstalled) return
253
- debuggerListenersInstalled = true
254
-
255
- chrome.debugger.onEvent.addListener((source, method, params) => {
256
- const tabId = source.tabId; if (!tabId) return
257
- const tab = tabs.get(tabId); if (!tab?.sessionId) return
258
- if (method === 'Target.attachedToTarget' && params?.sessionId) childSessionToTab.set(String(params.sessionId), tabId)
259
- if (method === 'Target.detachedFromTarget' && params?.sessionId) childSessionToTab.delete(String(params.sessionId))
260
- try {
261
- sendToRelay({ method: 'forwardCDPEvent', params: { sessionId: source.sessionId || tab.sessionId, method, params } })
262
- } catch { /* ignore */ }
263
- })
398
+ function onDebuggerEvent(source, method, params) {
399
+ const tabId = source.tabId
400
+ if (!tabId) return
401
+ const tab = tabs.get(tabId)
402
+ if (!tab?.sessionId) return
264
403
 
265
- chrome.debugger.onDetach.addListener((source, reason) => {
266
- if (source.tabId && tabs.has(source.tabId)) void detachTab(source.tabId, reason)
267
- })
268
- }
404
+ if (method === 'Target.attachedToTarget' && params?.sessionId) {
405
+ childSessionToTab.set(String(params.sessionId), tabId)
406
+ }
269
407
 
270
- // ---------------------------------------------------------------------------
271
- // Auto-attach: seamlessly attach to all tabs
272
- // ---------------------------------------------------------------------------
408
+ if (method === 'Target.detachedFromTarget' && params?.sessionId) {
409
+ childSessionToTab.delete(String(params.sessionId))
410
+ }
273
411
 
274
- async function autoAttachTab(tabId) {
275
- if (tabs.has(tabId)) return
276
- if (!relayConnected) return
277
412
  try {
278
- installDebuggerListeners()
279
- await attachTab(tabId)
280
- } catch { /* silently skip unattachable tabs */ }
281
- }
282
-
283
- async function autoAttachAllTabs() {
284
- if (!relayConnected) return
285
- installDebuggerListeners()
286
- const allTabs = await chrome.tabs.query({})
287
- for (const tab of allTabs) {
288
- if (tab.id && tab.url && !tab.url.startsWith('chrome://') && !tab.url.startsWith('chrome-extension://') && !tab.url.startsWith('devtools://')) {
289
- void autoAttachTab(tab.id)
290
- }
413
+ sendToRelay({
414
+ method: 'forwardCDPEvent',
415
+ params: {
416
+ sessionId: source.sessionId || tab.sessionId,
417
+ method,
418
+ params,
419
+ },
420
+ })
421
+ } catch {
422
+ // ignore
291
423
  }
292
424
  }
293
425
 
294
- // Auto-attach on tab events
295
- chrome.tabs.onUpdated.addListener((tabId, info, tab) => {
296
- if (info.status === 'complete' && tab.url && !tab.url.startsWith('chrome://')) void autoAttachTab(tabId)
297
- })
298
- chrome.tabs.onActivated.addListener(({ tabId }) => void autoAttachTab(tabId))
299
-
300
- // Manual toggle
301
- chrome.action.onClicked.addListener(async () => {
302
- const [active] = await chrome.tabs.query({ active: true, currentWindow: true })
303
- if (!active?.id) return
304
- if (tabs.has(active.id)) { await detachTab(active.id, 'toggle') }
305
- else { void autoAttachTab(active.id) }
306
- })
307
-
308
- // ---------------------------------------------------------------------------
309
- // Startup: create offscreen document and connect
310
- // ---------------------------------------------------------------------------
311
-
312
- async function init() {
313
- await ensureOffscreen()
314
- ensureOffscreenPort()
426
+ function onDebuggerDetach(source, reason) {
427
+ const tabId = source.tabId
428
+ if (!tabId) return
429
+ if (!tabs.has(tabId)) return
430
+ void detachTab(tabId, reason)
315
431
  }
316
432
 
317
- chrome.runtime.onStartup.addListener(() => void init())
318
- chrome.runtime.onInstalled.addListener(() => void init())
433
+ chrome.action.onClicked.addListener(() => void connectOrToggleForActiveTab())
319
434
 
320
- // Keepalive alarm to ensure offscreen doc stays alive
321
- chrome.alarms.create('skykoi-keepalive', { periodInMinutes: 0.5 })
322
- chrome.alarms.onAlarm.addListener((alarm) => {
323
- if (alarm.name === 'skykoi-keepalive') void init()
435
+ chrome.runtime.onInstalled.addListener(() => {
436
+ // Useful: first-time instructions.
437
+ void chrome.runtime.openOptionsPage()
324
438
  })
325
-
326
- // Fire immediately
327
- void init()
@@ -1,17 +1,17 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "SKYKOI Browser Relay",
4
- "version": "0.2.0",
5
- "description": "Attach SKYKOI to your existing Chrome tabs via a local CDP relay server.",
4
+ "version": "0.1.0",
5
+ "description": "Attach SKYKOI to your existing Chrome tab via a local CDP relay server.",
6
6
  "icons": {
7
7
  "16": "icons/icon16.png",
8
8
  "32": "icons/icon32.png",
9
9
  "48": "icons/icon48.png",
10
10
  "128": "icons/icon128.png"
11
11
  },
12
- "permissions": ["debugger", "tabs", "activeTab", "storage", "alarms", "offscreen"],
12
+ "permissions": ["debugger", "tabs", "activeTab", "storage"],
13
13
  "host_permissions": ["http://127.0.0.1/*", "http://localhost/*"],
14
- "background": { "service_worker": "background.js" },
14
+ "background": { "service_worker": "background.js", "type": "module" },
15
15
  "action": {
16
16
  "default_title": "SKYKOI Browser Relay (click to attach/detach)",
17
17
  "default_icon": {