@duheso/zerocli 0.8.6 → 0.8.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.
@@ -6,6 +6,8 @@ const VERSION = '1.0.0';
6
6
 
7
7
  let nativePort = null;
8
8
  let isConnected = false;
9
+ let isConnecting = false; // guard against concurrent connect attempts
10
+ let reconnectTimer = null; // handle for pending reconnect setTimeout
9
11
  let pendingRequests = new Map(); // requestId -> { resolve, reject, timeoutId }
10
12
  let requestCounter = 0;
11
13
  let debuggerAttachedTabs = new Set();
@@ -14,53 +16,114 @@ let networkRequests = new Map(); // tabId -> [requests]
14
16
  let captureStreams = new Map(); // tabId -> MediaRecorder
15
17
  let gifFrames = new Map(); // requestId -> [frames]
16
18
 
17
- // ─── Native Messaging ──────────────────────────────────────────────────────
19
+ // ─── Debug Stats ──────────────────────────────────────────────────────────────
20
+
21
+ const debugStats = {
22
+ connectAttempts: 0,
23
+ connectSuccesses: 0,
24
+ disconnects: 0,
25
+ toolsDispatched: 0,
26
+ toolErrors: 0,
27
+ lastError: null,
28
+ lastErrorTime: null,
29
+ lastToolCall: null,
30
+ lastToolCallTime: null,
31
+ workerStartTime: Date.now(),
32
+ };
33
+
34
+ function dbg(msg, ...args) {
35
+ const ts = new Date().toISOString();
36
+ console.log(`[ZeroCLI ${ts}] ${msg}`, ...args);
37
+ }
38
+
39
+ function scheduleReconnect(delayMs) {
40
+ // Cancel any pending reconnect to avoid stacking timers
41
+ if (reconnectTimer !== null) {
42
+ clearTimeout(reconnectTimer);
43
+ reconnectTimer = null;
44
+ }
45
+ reconnectTimer = setTimeout(() => {
46
+ reconnectTimer = null;
47
+ connectNativeHost();
48
+ }, delayMs);
49
+ }
18
50
 
19
51
  function connectNativeHost() {
52
+ // Guard: don't start a new connection if one is already in progress or live
53
+ if (isConnecting || isConnected) {
54
+ dbg(`connectNativeHost skipped (isConnecting=${isConnecting}, isConnected=${isConnected})`);
55
+ return;
56
+ }
57
+ isConnecting = true;
58
+ debugStats.connectAttempts++;
59
+ dbg(`Connecting to native host (attempt #${debugStats.connectAttempts})...`);
20
60
  try {
21
- nativePort = chrome.runtime.connectNative(NATIVE_HOST_NAME);
22
- isConnected = true;
23
- updateIcon(true);
61
+ const port = chrome.runtime.connectNative(NATIVE_HOST_NAME);
62
+ nativePort = port;
24
63
 
25
- nativePort.onMessage.addListener((message) => {
64
+ port.onMessage.addListener((message) => {
26
65
  handleNativeMessage(message);
27
66
  });
28
67
 
29
- nativePort.onDisconnect.addListener(() => {
68
+ port.onDisconnect.addListener(() => {
30
69
  // Read lastError to mark it as "checked" and suppress DevTools warning
31
70
  const err = chrome.runtime.lastError;
71
+ const errMsg = err?.message ?? 'unknown reason';
32
72
  const hostNotFound = err && (
33
73
  err.message?.includes('not found') ||
34
74
  err.message?.includes('Cannot find native messaging host') ||
35
75
  err.message?.includes('Specified native messaging host not found')
36
76
  );
77
+
78
+ // Only process disconnect for the port we currently own
79
+ if (nativePort !== port) {
80
+ dbg(`Stale port disconnect ignored: ${errMsg}`);
81
+ return;
82
+ }
83
+
84
+ debugStats.disconnects++;
85
+ debugStats.lastError = errMsg;
86
+ debugStats.lastErrorTime = Date.now();
37
87
  isConnected = false;
88
+ isConnecting = false;
38
89
  nativePort = null;
39
90
  updateIcon(false);
91
+ dbg(`Native host disconnected (total disconnects: ${debugStats.disconnects}): ${errMsg}`);
92
+
40
93
  // Reject all pending requests
41
- for (const [id, pending] of pendingRequests) {
94
+ for (const [, pending] of pendingRequests) {
42
95
  clearTimeout(pending.timeoutId);
43
96
  pending.reject(new Error(err?.message ?? 'Native host disconnected'));
44
97
  }
45
98
  pendingRequests.clear();
99
+
46
100
  if (hostNotFound) {
47
- // Native host not installed yet — retry slowly (every 30s)
48
- // User needs to run: zero --chrome (to register the native host)
49
101
  console.warn('[ZeroCLI] Native messaging host not found. Run "zero --chrome" once to register it, then reload this extension.');
50
- setTimeout(connectNativeHost, 30000);
102
+ scheduleReconnect(30000);
51
103
  } else {
52
- // Normal disconnect — reconnect after short delay
53
- setTimeout(connectNativeHost, 3000);
104
+ scheduleReconnect(3000);
54
105
  }
55
106
  });
56
107
 
108
+ // Mark connected only after port is set up (Chrome fires onDisconnect
109
+ // synchronously if the host binary isn't found, so this must come after)
110
+ isConnected = true;
111
+ isConnecting = false;
112
+ debugStats.connectSuccesses++;
113
+ updateIcon(true);
114
+ dbg('Native host connected successfully.');
115
+
57
116
  // Send initial ping
58
117
  sendToNative({ type: 'ping' });
59
118
  } catch (err) {
60
119
  isConnected = false;
120
+ isConnecting = false;
121
+ nativePort = null;
61
122
  updateIcon(false);
123
+ debugStats.lastError = err?.message ?? String(err);
124
+ debugStats.lastErrorTime = Date.now();
62
125
  console.warn('[ZeroCLI] connectNativeHost error:', err?.message);
63
- setTimeout(connectNativeHost, 30000);
126
+ scheduleReconnect(30000);
64
127
  }
65
128
  }
66
129
 
@@ -79,7 +142,7 @@ async function handleNativeMessage(message) {
79
142
 
80
143
  switch (message.type) {
81
144
  case 'pong':
82
- // Connection confirmed
145
+ dbg('Received pong from native host — connection healthy.');
83
146
  break;
84
147
 
85
148
  case 'status_response':
@@ -88,14 +151,23 @@ async function handleNativeMessage(message) {
88
151
 
89
152
  case 'tool_request': {
90
153
  const { method, params, id: requestId } = message;
154
+ debugStats.toolsDispatched++;
155
+ debugStats.lastToolCall = method;
156
+ debugStats.lastToolCallTime = Date.now();
157
+ dbg(`Tool request #${debugStats.toolsDispatched}: ${method} (id=${requestId})`);
91
158
  try {
92
159
  const result = await dispatchTool(method, params || {});
160
+ dbg(`Tool success: ${method}`);
93
161
  sendToNative({
94
162
  type: 'tool_response',
95
163
  id: requestId,
96
164
  result,
97
165
  });
98
166
  } catch (err) {
167
+ debugStats.toolErrors++;
168
+ debugStats.lastError = `${method}: ${err.message}`;
169
+ debugStats.lastErrorTime = Date.now();
170
+ console.error(`[ZeroCLI] Tool error: ${method}:`, err.message);
99
171
  sendToNative({
100
172
  type: 'tool_response',
101
173
  id: requestId,
@@ -106,10 +178,12 @@ async function handleNativeMessage(message) {
106
178
  }
107
179
 
108
180
  case 'mcp_connected':
181
+ dbg('MCP client connected.');
109
182
  updateIcon(true);
110
183
  break;
111
184
 
112
185
  case 'mcp_disconnected':
186
+ dbg('MCP client disconnected.');
113
187
  updateIcon(false);
114
188
  break;
115
189
 
@@ -156,6 +230,8 @@ async function dispatchTool(method, params) {
156
230
  return toolShortcutsList(params);
157
231
  case 'shortcuts_execute':
158
232
  return toolShortcutsExecute(params);
233
+ case 'debug_info':
234
+ return toolDebugInfo(params);
159
235
  default:
160
236
  throw new Error(`Unknown tool: ${method}`);
161
237
  }
@@ -467,6 +543,40 @@ async function toolShortcutsExecute(params) {
467
543
  return result;
468
544
  }
469
545
 
546
+ async function toolDebugInfo(_params) {
547
+ const uptimeMs = Date.now() - debugStats.workerStartTime;
548
+ const tabs = await chrome.tabs.query({}).catch(() => []);
549
+ return {
550
+ version: VERSION,
551
+ isConnected,
552
+ nativePortAlive: nativePort !== null,
553
+ uptime_ms: uptimeMs,
554
+ uptime_human: `${Math.floor(uptimeMs / 60000)}m ${Math.floor((uptimeMs % 60000) / 1000)}s`,
555
+ stats: {
556
+ connectAttempts: debugStats.connectAttempts,
557
+ connectSuccesses: debugStats.connectSuccesses,
558
+ disconnects: debugStats.disconnects,
559
+ toolsDispatched: debugStats.toolsDispatched,
560
+ toolErrors: debugStats.toolErrors,
561
+ },
562
+ flags: {
563
+ isConnected,
564
+ isConnecting,
565
+ hasPendingReconnectTimer: reconnectTimer !== null,
566
+ },
567
+ lastError: debugStats.lastError,
568
+ lastErrorAgo: debugStats.lastErrorTime
569
+ ? `${Math.round((Date.now() - debugStats.lastErrorTime) / 1000)}s ago`
570
+ : null,
571
+ lastToolCall: debugStats.lastToolCall,
572
+ lastToolCallAgo: debugStats.lastToolCallTime
573
+ ? `${Math.round((Date.now() - debugStats.lastToolCallTime) / 1000)}s ago`
574
+ : null,
575
+ openTabs: tabs.length,
576
+ debuggerAttachedTabs: debuggerAttachedTabs.size,
577
+ };
578
+ }
579
+
470
580
  // ─── Helper Functions ────────────────────────────────────────────────────────
471
581
 
472
582
  async function getActiveTabId() {
@@ -492,10 +602,14 @@ async function waitForTabLoad(tabId, timeoutMs = 10000) {
492
602
  }
493
603
 
494
604
  async function executeInTab(tabId, func, args) {
605
+ // chrome.scripting.executeScript requires all args to be structured-clone
606
+ // serializable. undefined is NOT serializable, so replace with null.
607
+ // The injected functions already treat null as "not provided" (falsy checks).
608
+ const safeArgs = args.map(v => v === undefined ? null : v);
495
609
  const results = await chrome.scripting.executeScript({
496
610
  target: { tabId },
497
611
  func,
498
- args,
612
+ args: safeArgs,
499
613
  });
500
614
  return results[0]?.result;
501
615
  }
@@ -906,23 +1020,50 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
906
1020
  if (message?.type === 'get_status') {
907
1021
  sendResponse({
908
1022
  isConnected,
1023
+ isConnecting,
909
1024
  nativePortAlive: nativePort !== null,
910
1025
  version: VERSION,
1026
+ stats: { ...debugStats },
911
1027
  });
912
1028
  }
913
1029
  // Return false: we handled synchronously.
914
1030
  return false;
915
1031
  });
916
1032
 
1033
+ // ─── Service Worker Keep-Alive (MV3) ─────────────────────────────────────────
1034
+ //
1035
+ // Chrome MV3 service workers are killed after ~30s of inactivity. We use the
1036
+ // alarms API to wake the worker every 20 seconds, keeping the native messaging
1037
+ // connection alive. Without this, the native host process exits, the Unix socket
1038
+ // disappears, and all tool calls fail with "Browser extension is not connected".
1039
+
1040
+ chrome.alarms.create('zerocli-keepalive', { periodInMinutes: 0.33 }); // ~20 seconds
1041
+
1042
+ chrome.alarms.onAlarm.addListener((alarm) => {
1043
+ if (alarm.name === 'zerocli-keepalive') {
1044
+ if (isConnected) {
1045
+ // Send a lightweight ping to verify the connection is still healthy
1046
+ sendToNative({ type: 'ping' });
1047
+ } else if (!isConnecting && reconnectTimer === null) {
1048
+ // Only reconnect if nothing else has already scheduled one
1049
+ dbg('Keep-alive alarm: not connected, scheduling reconnect...');
1050
+ scheduleReconnect(0);
1051
+ }
1052
+ }
1053
+ });
1054
+
917
1055
  // ─── Init ────────────────────────────────────────────────────────────────────
918
1056
 
919
1057
  chrome.runtime.onInstalled.addListener(() => {
920
1058
  updateIcon(false);
1059
+ dbg('Extension installed/updated.');
921
1060
  });
922
1061
 
923
1062
  chrome.runtime.onStartup.addListener(() => {
1063
+ dbg('Browser startup — connecting native host.');
924
1064
  connectNativeHost();
925
1065
  });
926
1066
 
927
1067
  // Connect when extension loads
1068
+ dbg('Service worker started.');
928
1069
  connectNativeHost();
@@ -39,7 +39,8 @@
39
39
  "storage",
40
40
  "tabCapture",
41
41
  "windows",
42
- "notifications"
42
+ "notifications",
43
+ "alarms"
43
44
  ],
44
45
  "host_permissions": [
45
46
  "<all_urls>"