surf-cli 2.8.0 → 2.9.0
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 +98 -4
- package/dist/content/index.js +116 -0
- package/dist/content/index.js.map +1 -0
- package/dist/manifest.json +2 -11
- package/dist/options/options.js +3 -3
- package/dist/options/options.js.map +1 -1
- package/dist/service-worker/index.js +261 -61
- package/dist/service-worker/index.js.map +1 -1
- package/native/abort.cjs +65 -0
- package/native/ai-queue.cjs +64 -0
- package/native/aistudio-build.cjs +21 -13
- package/native/aistudio-client.cjs +40 -20
- package/native/browser-lock.cjs +2 -2
- package/native/chatgpt-client.cjs +47 -31
- package/native/cli.cjs +300 -204
- package/native/client-transport.cjs +168 -0
- package/native/do-executor.cjs +25 -44
- package/native/doctor.cjs +55 -5
- package/native/endpoint.cjs +174 -0
- package/native/file-transfer.cjs +734 -0
- package/native/gemini-client.cjs +156 -71
- package/native/grok-client.cjs +98 -89
- package/native/host-helpers.cjs +37 -12
- package/native/host-sessions.cjs +283 -0
- package/native/host.cjs +800 -620
- package/native/listener.cjs +20 -0
- package/native/mcp-server.cjs +60 -65
- package/native/network-export.cjs +113 -0
- package/native/perplexity-client.cjs +46 -17
- package/native/remote-auth.cjs +279 -0
- package/native/remote-transport.cjs +337 -0
- package/native/request-pending.cjs +148 -0
- package/native/socket-path.cjs +1 -1
- package/package.json +8 -6
- package/scripts/install-native-host.cjs +36 -5
- package/skills/README.md +11 -5
- package/skills/deep-x-research/SKILL.md +106 -0
- package/skills/surf/SKILL.md +31 -4
- package/dist/content/accessibility-tree.js +0 -11
- package/dist/content/accessibility-tree.js.map +0 -1
- package/dist/content/visual-indicator.js +0 -111
- package/dist/content/visual-indicator.js.map +0 -1
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
const { abortError } = require("./abort.cjs");
|
|
2
|
+
|
|
3
|
+
const MAX_CONNECTIONS = 32;
|
|
4
|
+
const MAX_REMOTE_CONNECTIONS = 16;
|
|
5
|
+
const MAX_PRINCIPAL_CONNECTIONS = 4;
|
|
6
|
+
const MAX_WAITERS = 16;
|
|
7
|
+
const MAX_STREAMS_PER_PRINCIPAL = 2;
|
|
8
|
+
const MAX_REMOTE_STREAMS = 4;
|
|
9
|
+
const MAX_STREAMS = 4;
|
|
10
|
+
const REQUEST_ID_LIMIT = 128;
|
|
11
|
+
const TOOL_NAME_LIMIT = 128;
|
|
12
|
+
const LEASE_IDLE_MS = 5000;
|
|
13
|
+
const AUTHENTICATED_IDLE_MS = 15000;
|
|
14
|
+
const QUEUE_TIMEOUT_MS = 60000;
|
|
15
|
+
const DEFAULT_DEADLINE_MS = 60000;
|
|
16
|
+
const MAX_DEADLINE_MS = 50 * 60 * 1000;
|
|
17
|
+
const CLEANUP_GRACE_MS = 60000;
|
|
18
|
+
const PROVIDER_DEFAULT_TIMEOUT_SECONDS = {
|
|
19
|
+
ai: 300,
|
|
20
|
+
aistudio: 300,
|
|
21
|
+
"aistudio.build": 600,
|
|
22
|
+
chatgpt: 2700,
|
|
23
|
+
gemini: 300,
|
|
24
|
+
grok: 300,
|
|
25
|
+
perplexity: 120,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
function resolveRequestDeadlineMs(tool, args = {}) {
|
|
29
|
+
const defaultSeconds = PROVIDER_DEFAULT_TIMEOUT_SECONDS[tool];
|
|
30
|
+
if (defaultSeconds === undefined) return DEFAULT_DEADLINE_MS;
|
|
31
|
+
const requestedSeconds = Number(
|
|
32
|
+
args && typeof args === "object" && !Array.isArray(args) ? args.timeout : undefined,
|
|
33
|
+
);
|
|
34
|
+
const seconds = Number.isFinite(requestedSeconds) && requestedSeconds > 0
|
|
35
|
+
? requestedSeconds
|
|
36
|
+
: defaultSeconds;
|
|
37
|
+
return Math.min(seconds * 1000 + CLEANUP_GRACE_MS, MAX_DEADLINE_MS);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
class HostSessionManager {
|
|
41
|
+
constructor({ audit = () => {}, onTimeout = () => {} } = {}) {
|
|
42
|
+
this.audit = audit;
|
|
43
|
+
this.onTimeout = onTimeout;
|
|
44
|
+
this.contexts = new Set();
|
|
45
|
+
this.principalCounts = new Map();
|
|
46
|
+
this.waiters = [];
|
|
47
|
+
this.leaseOwner = null;
|
|
48
|
+
this.remoteConnections = 0;
|
|
49
|
+
this.streams = new Set();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
admit(socket, isRemote) {
|
|
53
|
+
if (this.contexts.size >= MAX_CONNECTIONS) throw new Error("maximum client connections reached");
|
|
54
|
+
if (isRemote && this.remoteConnections >= MAX_REMOTE_CONNECTIONS) throw new Error("maximum remote connections reached");
|
|
55
|
+
const context = {
|
|
56
|
+
socket,
|
|
57
|
+
isRemote,
|
|
58
|
+
principal: null,
|
|
59
|
+
closed: false,
|
|
60
|
+
activeRequest: null,
|
|
61
|
+
seenRequestIds: new Set(),
|
|
62
|
+
idleTimer: null,
|
|
63
|
+
workTimer: null,
|
|
64
|
+
admitted: true,
|
|
65
|
+
stream: false,
|
|
66
|
+
};
|
|
67
|
+
this.contexts.add(context);
|
|
68
|
+
if (isRemote) this.remoteConnections += 1;
|
|
69
|
+
this.audit({ event: "connection", context, outcome: "accepted" });
|
|
70
|
+
return context;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
authenticate(context, principal) {
|
|
74
|
+
if (context.closed) throw new Error("connection is closed");
|
|
75
|
+
const count = this.principalCounts.get(principal.clientId) || 0;
|
|
76
|
+
if (count >= MAX_PRINCIPAL_CONNECTIONS) throw new Error("maximum connections for remote principal reached");
|
|
77
|
+
context.principal = principal;
|
|
78
|
+
this.principalCounts.set(principal.clientId, count + 1);
|
|
79
|
+
context.workTimer = setTimeout(() => {
|
|
80
|
+
if (!context.closed && !context.activeRequest) {
|
|
81
|
+
this.audit({ event: "connection", context, outcome: "authenticated-idle-timeout" });
|
|
82
|
+
context.socket.destroy();
|
|
83
|
+
}
|
|
84
|
+
}, AUTHENTICATED_IDLE_MS);
|
|
85
|
+
this.audit({ event: "authentication", context, outcome: "success" });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
touch(context) {
|
|
89
|
+
if (!context || context.closed || !context.isRemote || context.activeRequest) return;
|
|
90
|
+
if (context.workTimer) clearTimeout(context.workTimer);
|
|
91
|
+
context.workTimer = setTimeout(() => {
|
|
92
|
+
if (!context.closed && !context.activeRequest) {
|
|
93
|
+
this.audit({ event: "connection", context, outcome: "authenticated-idle-timeout" });
|
|
94
|
+
context.socket.destroy();
|
|
95
|
+
}
|
|
96
|
+
}, AUTHENTICATED_IDLE_MS);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
canStartStream(context) {
|
|
100
|
+
if (context.closed || context.stream || context.activeRequest || this.leaseOwner === context) return false;
|
|
101
|
+
const principalId = context.principal?.clientId || "local";
|
|
102
|
+
const principalStreams = [...this.streams].filter((entry) => entry.principalId === principalId).length;
|
|
103
|
+
if (principalStreams >= MAX_STREAMS_PER_PRINCIPAL) return false;
|
|
104
|
+
if (this.streams.size >= MAX_STREAMS) return false;
|
|
105
|
+
if (context.isRemote && [...this.streams].filter((entry) => entry.isRemote).length >= MAX_REMOTE_STREAMS) return false;
|
|
106
|
+
if (context.workTimer) {
|
|
107
|
+
clearTimeout(context.workTimer);
|
|
108
|
+
context.workTimer = null;
|
|
109
|
+
}
|
|
110
|
+
context.stream = true;
|
|
111
|
+
this.streams.add({ context, principalId, isRemote: context.isRemote });
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
stopStream(context) {
|
|
116
|
+
for (const entry of this.streams) if (entry.context === context) this.streams.delete(entry);
|
|
117
|
+
context.stream = false;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
beginRequest(context, { id, tool, deadlineMs }) {
|
|
121
|
+
if (context.closed) return Promise.reject(new Error("connection is closed"));
|
|
122
|
+
if (context.workTimer) {
|
|
123
|
+
clearTimeout(context.workTimer);
|
|
124
|
+
context.workTimer = null;
|
|
125
|
+
}
|
|
126
|
+
if (context.stream) return Promise.reject(new Error("stream connection cannot execute tools"));
|
|
127
|
+
if (typeof id !== "string" || !id) return Promise.reject(new Error("request id is required"));
|
|
128
|
+
if (id.length > REQUEST_ID_LIMIT) return Promise.reject(new Error("request ID is too long"));
|
|
129
|
+
if (typeof tool !== "string" || !tool) return Promise.reject(new Error("tool name is required"));
|
|
130
|
+
if (tool.length > TOOL_NAME_LIMIT) return Promise.reject(new Error("tool name is too long"));
|
|
131
|
+
if (context.seenRequestIds.has(id)) return Promise.reject(new Error("duplicate request id"));
|
|
132
|
+
if (context.seenRequestIds.size >= REQUEST_ID_LIMIT) return Promise.reject(new Error("request ID limit reached"));
|
|
133
|
+
if (context.activeRequest) return Promise.reject(new Error("one in-flight request per connection"));
|
|
134
|
+
context.seenRequestIds.add(id);
|
|
135
|
+
const controller = new AbortController();
|
|
136
|
+
const request = {
|
|
137
|
+
id,
|
|
138
|
+
tool,
|
|
139
|
+
startedAt: Date.now(),
|
|
140
|
+
deadlineMs: Math.min(Math.max(deadlineMs || DEFAULT_DEADLINE_MS, 1), MAX_DEADLINE_MS),
|
|
141
|
+
queued: true,
|
|
142
|
+
settled: false,
|
|
143
|
+
controller,
|
|
144
|
+
signal: controller.signal,
|
|
145
|
+
tombstoned: false,
|
|
146
|
+
};
|
|
147
|
+
context.activeRequest = request;
|
|
148
|
+
const grant = () => {
|
|
149
|
+
if (context.closed) return Promise.reject(new Error("connection closed while waiting for browser lease"));
|
|
150
|
+
request.queued = false;
|
|
151
|
+
request.timer = setTimeout(() => this.onRequestTimeout(context, request), request.deadlineMs);
|
|
152
|
+
this.leaseOwner = context;
|
|
153
|
+
this.audit({ event: "lease", context, request, outcome: "acquired" });
|
|
154
|
+
return Promise.resolve(request);
|
|
155
|
+
};
|
|
156
|
+
if (!this.leaseOwner || this.leaseOwner === context) {
|
|
157
|
+
if (context.idleTimer) clearTimeout(context.idleTimer);
|
|
158
|
+
context.idleTimer = null;
|
|
159
|
+
return grant();
|
|
160
|
+
}
|
|
161
|
+
if (this.waiters.length >= MAX_WAITERS) {
|
|
162
|
+
context.activeRequest = null;
|
|
163
|
+
return Promise.reject(new Error("browser lease queue is full"));
|
|
164
|
+
}
|
|
165
|
+
return new Promise((resolve, reject) => {
|
|
166
|
+
const waiter = { context, request, resolve, reject };
|
|
167
|
+
waiter.timer = setTimeout(() => {
|
|
168
|
+
const index = this.waiters.indexOf(waiter);
|
|
169
|
+
if (index !== -1) this.waiters.splice(index, 1);
|
|
170
|
+
if (context.activeRequest === request) context.activeRequest = null;
|
|
171
|
+
this.audit({ event: "lease", context, request, outcome: "queue-timeout" });
|
|
172
|
+
reject(new Error("timed out waiting for browser lease"));
|
|
173
|
+
}, QUEUE_TIMEOUT_MS);
|
|
174
|
+
this.waiters.push(waiter);
|
|
175
|
+
this.audit({ event: "lease", context, request, outcome: "queued" });
|
|
176
|
+
}).then(() => grant());
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
onRequestTimeout(context, request) {
|
|
180
|
+
if (context.activeRequest !== request || request.settled) return;
|
|
181
|
+
request.tombstoned = true;
|
|
182
|
+
if (!request.signal.aborted) request.controller.abort(abortError(null, "Request timed out"));
|
|
183
|
+
this.audit({ event: "request", context, request, outcome: "hard-timeout" });
|
|
184
|
+
this.onTimeout(context, request);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
canRespond(context, id) {
|
|
188
|
+
return Boolean(context && context.activeRequest && context.activeRequest.id === id && !context.activeRequest.settled);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
complete(context, id, outcome = "completed") {
|
|
192
|
+
const request = context?.activeRequest;
|
|
193
|
+
if (!request || request.id !== id || request.settled) return;
|
|
194
|
+
request.settled = true;
|
|
195
|
+
if (request.timer) clearTimeout(request.timer);
|
|
196
|
+
if (request.abortCleanup) request.abortCleanup();
|
|
197
|
+
context.activeRequest = null;
|
|
198
|
+
this.audit({ event: "request", context, request, outcome, elapsedMs: Date.now() - request.startedAt });
|
|
199
|
+
if (this.leaseOwner === context) {
|
|
200
|
+
if (context.closed) this.releaseLease(context);
|
|
201
|
+
else {
|
|
202
|
+
if (context.idleTimer) clearTimeout(context.idleTimer);
|
|
203
|
+
context.idleTimer = setTimeout(() => {
|
|
204
|
+
if (!context.activeRequest && this.leaseOwner === context) this.releaseLease(context);
|
|
205
|
+
}, LEASE_IDLE_MS);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
releaseLease(context) {
|
|
211
|
+
if (this.leaseOwner !== context) return;
|
|
212
|
+
if (context.idleTimer) clearTimeout(context.idleTimer);
|
|
213
|
+
context.idleTimer = null;
|
|
214
|
+
this.leaseOwner = null;
|
|
215
|
+
this.audit({ event: "lease", context, outcome: "released" });
|
|
216
|
+
while (this.waiters.length) {
|
|
217
|
+
const waiter = this.waiters.shift();
|
|
218
|
+
if (!waiter || waiter.context.closed) continue;
|
|
219
|
+
clearTimeout(waiter.timer);
|
|
220
|
+
this.leaseOwner = waiter.context;
|
|
221
|
+
waiter.resolve();
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
close(context) {
|
|
227
|
+
if (!context || context.closed) return;
|
|
228
|
+
context.closed = true;
|
|
229
|
+
if (context.workTimer) clearTimeout(context.workTimer);
|
|
230
|
+
context.workTimer = null;
|
|
231
|
+
this.stopStream(context);
|
|
232
|
+
if (context.principal) {
|
|
233
|
+
const count = this.principalCounts.get(context.principal.clientId) || 1;
|
|
234
|
+
if (count <= 1) this.principalCounts.delete(context.principal.clientId);
|
|
235
|
+
else this.principalCounts.set(context.principal.clientId, count - 1);
|
|
236
|
+
}
|
|
237
|
+
for (let index = this.waiters.length - 1; index >= 0; index -= 1) {
|
|
238
|
+
const waiter = this.waiters[index];
|
|
239
|
+
if (waiter.context !== context) continue;
|
|
240
|
+
this.waiters.splice(index, 1);
|
|
241
|
+
clearTimeout(waiter.timer);
|
|
242
|
+
waiter.reject(new Error("connection closed while waiting for browser lease"));
|
|
243
|
+
}
|
|
244
|
+
if (context.activeRequest?.queued) {
|
|
245
|
+
const request = context.activeRequest;
|
|
246
|
+
context.activeRequest = null;
|
|
247
|
+
if (!request.signal.aborted) request.controller.abort(abortError(null, "Request cancelled: queued client disconnected"));
|
|
248
|
+
this.audit({ event: "request", context, request, outcome: "queue-canceled" });
|
|
249
|
+
} else if (context.activeRequest) {
|
|
250
|
+
const request = context.activeRequest;
|
|
251
|
+
request.abandoned = true;
|
|
252
|
+
request.tombstoned = true;
|
|
253
|
+
if (!request.signal.aborted) request.controller.abort(abortError(null, "Request cancelled: client disconnected"));
|
|
254
|
+
this.audit({ event: "request", context, request, outcome: "abort-requested" });
|
|
255
|
+
this.audit({ event: "request", context, request, outcome: "abandoned" });
|
|
256
|
+
} else if (this.leaseOwner === context) {
|
|
257
|
+
this.releaseLease(context);
|
|
258
|
+
}
|
|
259
|
+
this.contexts.delete(context);
|
|
260
|
+
if (context.isRemote) this.remoteConnections -= 1;
|
|
261
|
+
this.audit({ event: "connection", context, outcome: "closed" });
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
module.exports = {
|
|
266
|
+
AUTHENTICATED_IDLE_MS,
|
|
267
|
+
DEFAULT_DEADLINE_MS,
|
|
268
|
+
HostSessionManager,
|
|
269
|
+
MAX_DEADLINE_MS,
|
|
270
|
+
PROVIDER_DEFAULT_TIMEOUT_SECONDS,
|
|
271
|
+
resolveRequestDeadlineMs,
|
|
272
|
+
LEASE_IDLE_MS,
|
|
273
|
+
MAX_CONNECTIONS,
|
|
274
|
+
MAX_PRINCIPAL_CONNECTIONS,
|
|
275
|
+
MAX_REMOTE_CONNECTIONS,
|
|
276
|
+
MAX_REMOTE_STREAMS,
|
|
277
|
+
MAX_STREAMS,
|
|
278
|
+
MAX_STREAMS_PER_PRINCIPAL,
|
|
279
|
+
TOOL_NAME_LIMIT,
|
|
280
|
+
MAX_WAITERS,
|
|
281
|
+
QUEUE_TIMEOUT_MS,
|
|
282
|
+
REQUEST_ID_LIMIT,
|
|
283
|
+
};
|