@duheso/zerocli 0.8.7 → 0.8.9

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();
@@ -34,21 +36,36 @@ function dbg(msg, ...args) {
34
36
  console.log(`[ZeroCLI ${ts}] ${msg}`, ...args);
35
37
  }
36
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
+ }
50
+
37
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;
38
58
  debugStats.connectAttempts++;
39
59
  dbg(`Connecting to native host (attempt #${debugStats.connectAttempts})...`);
40
60
  try {
41
- nativePort = chrome.runtime.connectNative(NATIVE_HOST_NAME);
42
- isConnected = true;
43
- debugStats.connectSuccesses++;
44
- updateIcon(true);
45
- dbg('Native host connected successfully.');
61
+ const port = chrome.runtime.connectNative(NATIVE_HOST_NAME);
62
+ nativePort = port;
46
63
 
47
- nativePort.onMessage.addListener((message) => {
64
+ port.onMessage.addListener((message) => {
48
65
  handleNativeMessage(message);
49
66
  });
50
67
 
51
- nativePort.onDisconnect.addListener(() => {
68
+ port.onDisconnect.addListener(() => {
52
69
  // Read lastError to mark it as "checked" and suppress DevTools warning
53
70
  const err = chrome.runtime.lastError;
54
71
  const errMsg = err?.message ?? 'unknown reason';
@@ -57,39 +74,56 @@ function connectNativeHost() {
57
74
  err.message?.includes('Cannot find native messaging host') ||
58
75
  err.message?.includes('Specified native messaging host not found')
59
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
+
60
84
  debugStats.disconnects++;
61
85
  debugStats.lastError = errMsg;
62
86
  debugStats.lastErrorTime = Date.now();
63
87
  isConnected = false;
88
+ isConnecting = false;
64
89
  nativePort = null;
65
90
  updateIcon(false);
66
91
  dbg(`Native host disconnected (total disconnects: ${debugStats.disconnects}): ${errMsg}`);
92
+
67
93
  // Reject all pending requests
68
- for (const [id, pending] of pendingRequests) {
94
+ for (const [, pending] of pendingRequests) {
69
95
  clearTimeout(pending.timeoutId);
70
96
  pending.reject(new Error(err?.message ?? 'Native host disconnected'));
71
97
  }
72
98
  pendingRequests.clear();
99
+
73
100
  if (hostNotFound) {
74
- // Native host not installed yet — retry slowly (every 30s)
75
- // User needs to run: zero --chrome (to register the native host)
76
101
  console.warn('[ZeroCLI] Native messaging host not found. Run "zero --chrome" once to register it, then reload this extension.');
77
- setTimeout(connectNativeHost, 30000);
102
+ scheduleReconnect(30000);
78
103
  } else {
79
- // Normal disconnect — reconnect after short delay
80
- setTimeout(connectNativeHost, 3000);
104
+ scheduleReconnect(3000);
81
105
  }
82
106
  });
83
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
+
84
116
  // Send initial ping
85
117
  sendToNative({ type: 'ping' });
86
118
  } catch (err) {
87
119
  isConnected = false;
120
+ isConnecting = false;
121
+ nativePort = null;
88
122
  updateIcon(false);
89
123
  debugStats.lastError = err?.message ?? String(err);
90
124
  debugStats.lastErrorTime = Date.now();
91
125
  console.warn('[ZeroCLI] connectNativeHost error:', err?.message);
92
- setTimeout(connectNativeHost, 30000);
126
+ scheduleReconnect(30000);
93
127
  }
94
128
  }
95
129
 
@@ -525,6 +559,11 @@ async function toolDebugInfo(_params) {
525
559
  toolsDispatched: debugStats.toolsDispatched,
526
560
  toolErrors: debugStats.toolErrors,
527
561
  },
562
+ flags: {
563
+ isConnected,
564
+ isConnecting,
565
+ hasPendingReconnectTimer: reconnectTimer !== null,
566
+ },
528
567
  lastError: debugStats.lastError,
529
568
  lastErrorAgo: debugStats.lastErrorTime
530
569
  ? `${Math.round((Date.now() - debugStats.lastErrorTime) / 1000)}s ago`
@@ -563,10 +602,14 @@ async function waitForTabLoad(tabId, timeoutMs = 10000) {
563
602
  }
564
603
 
565
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);
566
609
  const results = await chrome.scripting.executeScript({
567
610
  target: { tabId },
568
611
  func,
569
- args,
612
+ args: safeArgs,
570
613
  });
571
614
  return results[0]?.result;
572
615
  }
@@ -977,6 +1020,7 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
977
1020
  if (message?.type === 'get_status') {
978
1021
  sendResponse({
979
1022
  isConnected,
1023
+ isConnecting,
980
1024
  nativePortAlive: nativePort !== null,
981
1025
  version: VERSION,
982
1026
  stats: { ...debugStats },
@@ -997,12 +1041,13 @@ chrome.alarms.create('zerocli-keepalive', { periodInMinutes: 0.33 }); // ~20 sec
997
1041
 
998
1042
  chrome.alarms.onAlarm.addListener((alarm) => {
999
1043
  if (alarm.name === 'zerocli-keepalive') {
1000
- if (!isConnected) {
1001
- dbg('Keep-alive alarm: not connected, attempting reconnect...');
1002
- connectNativeHost();
1003
- } else {
1044
+ if (isConnected) {
1004
1045
  // Send a lightweight ping to verify the connection is still healthy
1005
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);
1006
1051
  }
1007
1052
  }
1008
1053
  });