@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.
- package/chrome-extension/background.js +156 -15
- package/chrome-extension/manifest.json +2 -1
- package/dist/cli.mjs +21466 -25258
- package/package.json +1 -1
|
@@ -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
|
-
// ───
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
updateIcon(true);
|
|
61
|
+
const port = chrome.runtime.connectNative(NATIVE_HOST_NAME);
|
|
62
|
+
nativePort = port;
|
|
24
63
|
|
|
25
|
-
|
|
64
|
+
port.onMessage.addListener((message) => {
|
|
26
65
|
handleNativeMessage(message);
|
|
27
66
|
});
|
|
28
67
|
|
|
29
|
-
|
|
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 [
|
|
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
|
-
|
|
102
|
+
scheduleReconnect(30000);
|
|
51
103
|
} else {
|
|
52
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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();
|