@ceki/sdk 1.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/dist/cli.js ADDED
@@ -0,0 +1,2658 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import * as fs4 from "fs";
5
+ import * as path3 from "path";
6
+
7
+ // src/client.ts
8
+ import WebSocket from "ws";
9
+
10
+ // src/config.ts
11
+ var defaults = {
12
+ apiUrl: "https://api.ceki.me",
13
+ relayUrl: "wss://browser.ceki.me/ws/agent",
14
+ chatUrl: "https://chat.ceki.me/api/chat"
15
+ };
16
+ function resolveConfig(opts) {
17
+ return {
18
+ apiUrl: opts?.apiUrl ?? process.env.CEKI_API_URL ?? defaults.apiUrl,
19
+ relayUrl: opts?.relayUrl ?? process.env.CEKI_RELAY_URL ?? defaults.relayUrl,
20
+ chatUrl: opts?.chatUrl ?? process.env.CEKI_CHAT_URL ?? defaults.chatUrl,
21
+ basicAuth: opts?.basicAuth ?? (process.env.CEKI_BASIC_AUTH_USER && process.env.CEKI_BASIC_AUTH_PASS ? [process.env.CEKI_BASIC_AUTH_USER, process.env.CEKI_BASIC_AUTH_PASS] : void 0),
22
+ reconnect: opts?.reconnect ?? true
23
+ };
24
+ }
25
+
26
+ // src/errors.ts
27
+ var CekiBrowserError = class extends Error {
28
+ constructor(message) {
29
+ super(message);
30
+ this.name = "CekiBrowserError";
31
+ }
32
+ };
33
+ var AuthError = class extends CekiBrowserError {
34
+ constructor(message = "Authentication failed") {
35
+ super(message);
36
+ this.name = "AuthError";
37
+ }
38
+ };
39
+ var SessionNotFound = class extends CekiBrowserError {
40
+ constructor(message = "Session not found") {
41
+ super(message);
42
+ this.name = "SessionNotFound";
43
+ }
44
+ };
45
+ var SessionExpired = class extends SessionNotFound {
46
+ constructor(message = "Session expired") {
47
+ super(message);
48
+ this.name = "SessionExpired";
49
+ }
50
+ };
51
+ var NotOwner = class extends CekiBrowserError {
52
+ constructor(message = "Not session owner") {
53
+ super(message);
54
+ this.name = "NotOwner";
55
+ }
56
+ };
57
+ var TransportError = class extends CekiBrowserError {
58
+ constructor(message = "Transport error") {
59
+ super(message);
60
+ this.name = "TransportError";
61
+ }
62
+ };
63
+ var TimeoutError = class extends CekiBrowserError {
64
+ constructor(message = "Operation timed out") {
65
+ super(message);
66
+ this.name = "TimeoutError";
67
+ }
68
+ };
69
+ var SessionEnded = class extends CekiBrowserError {
70
+ reason;
71
+ constructor(reason) {
72
+ super(`Session ended: ${reason}`);
73
+ this.name = "SessionEnded";
74
+ this.reason = reason;
75
+ }
76
+ };
77
+ var InsufficientFunds = class extends CekiBrowserError {
78
+ constructor(message = "Insufficient funds") {
79
+ super(message);
80
+ this.name = "InsufficientFunds";
81
+ }
82
+ };
83
+ var RateLimitExceeded = class extends CekiBrowserError {
84
+ retryAfter;
85
+ constructor(retryAfter = 0, message = "Rate limit exceeded") {
86
+ super(message);
87
+ this.name = "RateLimitExceeded";
88
+ this.retryAfter = retryAfter;
89
+ }
90
+ };
91
+ var ConnectionLost = class extends CekiBrowserError {
92
+ constructor(message = "Connection lost") {
93
+ super(message);
94
+ this.name = "ConnectionLost";
95
+ }
96
+ };
97
+ var ProviderOffline = class extends CekiBrowserError {
98
+ constructor(message = "Provider offline") {
99
+ super(message);
100
+ this.name = "ProviderOffline";
101
+ }
102
+ };
103
+ var CaptchaError = class extends CekiBrowserError {
104
+ constructor(message = "Captcha error") {
105
+ super(message);
106
+ this.name = "CaptchaError";
107
+ }
108
+ };
109
+ var CaptchaTimeoutError = class extends CaptchaError {
110
+ phase;
111
+ constructor(phase) {
112
+ super(`Captcha timeout: ${phase}`);
113
+ this.name = "CaptchaTimeoutError";
114
+ this.phase = phase;
115
+ }
116
+ };
117
+ var ChatSendFailed = class extends CekiBrowserError {
118
+ status;
119
+ messageText;
120
+ constructor(status, messageText) {
121
+ super(`Chat send failed (${status})`);
122
+ this.name = "ChatSendFailed";
123
+ this.status = status;
124
+ this.messageText = messageText;
125
+ }
126
+ };
127
+
128
+ // src/chat.ts
129
+ import * as crypto from "crypto";
130
+ import * as fs from "fs";
131
+ import * as path from "path";
132
+ var MAX_IMAGE_SIZE = 5 * 1024 * 1024;
133
+ function detectMime(buf) {
134
+ if (buf.length >= 4 && buf[0] === 137 && buf[1] === 80 && buf[2] === 78 && buf[3] === 71) {
135
+ return { mime: "image/png", ext: "png" };
136
+ }
137
+ if (buf.length >= 3 && buf[0] === 255 && buf[1] === 216 && buf[2] === 255) {
138
+ return { mime: "image/jpeg", ext: "jpg" };
139
+ }
140
+ if (buf.length >= 12 && buf[0] === 82 && buf[1] === 73 && buf[2] === 70 && buf[3] === 70 && buf[8] === 87 && buf[9] === 69 && buf[10] === 66 && buf[11] === 80) {
141
+ return { mime: "image/webp", ext: "webp" };
142
+ }
143
+ return { mime: "application/octet-stream", ext: "bin" };
144
+ }
145
+ function randomHex(len) {
146
+ return crypto.randomBytes(len / 2).toString("hex");
147
+ }
148
+ var BrowserChat = class {
149
+ _browser;
150
+ _topicId;
151
+ _messageHandlers = [];
152
+ _readHandlers = [];
153
+ _pendingSends = /* @__PURE__ */ new Map();
154
+ /** @internal */
155
+ _actionCallbacks = /* @__PURE__ */ new Map();
156
+ constructor(browser) {
157
+ this._browser = browser;
158
+ this._topicId = browser.chatTopicId;
159
+ }
160
+ get topicId() {
161
+ return this._topicId;
162
+ }
163
+ async send(text) {
164
+ const clientMsgId = randomHex(32);
165
+ const msg = {
166
+ type: "chat.send",
167
+ session_id: this._browser.sessionId,
168
+ client_msg_id: clientMsgId,
169
+ text
170
+ };
171
+ return new Promise((resolve2, reject) => {
172
+ const timer = setTimeout(() => {
173
+ this._pendingSends.delete(clientMsgId);
174
+ reject(new TimeoutError("Chat send timed out"));
175
+ }, 15e3);
176
+ this._pendingSends.set(clientMsgId, { resolve: resolve2, reject, timer });
177
+ this._browser._sendRaw(msg);
178
+ });
179
+ }
180
+ async sendImage(source, text) {
181
+ let buf;
182
+ let filename;
183
+ if (typeof source === "string") {
184
+ buf = fs.readFileSync(source);
185
+ filename = path.basename(source);
186
+ } else {
187
+ buf = Buffer.isBuffer(source) ? source : Buffer.from(source);
188
+ filename = "image";
189
+ }
190
+ if (buf.length > MAX_IMAGE_SIZE) {
191
+ throw new Error(`Image too large: ${buf.length} bytes (max ${MAX_IMAGE_SIZE})`);
192
+ }
193
+ const { mime, ext } = detectMime(buf);
194
+ if (!filename.includes(".")) {
195
+ filename = `${filename}.${ext}`;
196
+ }
197
+ const clientMsgId = randomHex(32);
198
+ const data_b64 = buf.toString("base64");
199
+ const msg = {
200
+ type: "chat.send_image",
201
+ session_id: this._browser.sessionId,
202
+ client_msg_id: clientMsgId,
203
+ filename,
204
+ mime,
205
+ data_b64
206
+ };
207
+ if (text) msg.text = text;
208
+ return new Promise((resolve2, reject) => {
209
+ const timer = setTimeout(() => {
210
+ this._pendingSends.delete(clientMsgId);
211
+ reject(new TimeoutError("Chat sendImage timed out"));
212
+ }, 15e3);
213
+ this._pendingSends.set(clientMsgId, { resolve: resolve2, reject, timer });
214
+ this._browser._sendRaw(msg);
215
+ });
216
+ }
217
+ onMessage(cb) {
218
+ this._messageHandlers.push(cb);
219
+ }
220
+ onRead(cb) {
221
+ this._readHandlers.push(cb);
222
+ }
223
+ async history(opts) {
224
+ if (!this._topicId) return [];
225
+ const params = new URLSearchParams();
226
+ params.set("topic_id", this._topicId);
227
+ if (opts?.limit != null) params.set("limit", String(opts.limit));
228
+ if (opts?.beforeId) params.set("before", opts.beforeId);
229
+ if (opts?.since) params.set("since", opts.since);
230
+ const url = `${this._browser._chatUrl}/messages?${params.toString()}`;
231
+ const headers = {
232
+ "Authorization": `Bearer ${this._browser._apiKey}`
233
+ };
234
+ const basicAuth = this._browser._basicAuth;
235
+ if (basicAuth) {
236
+ const encoded = Buffer.from(`${basicAuth[0]}:${basicAuth[1]}`).toString("base64");
237
+ headers["X-Basic-Auth"] = `Basic ${encoded}`;
238
+ }
239
+ const resp = await fetch(url, { headers });
240
+ if (!resp.ok) {
241
+ throw new Error(`Chat history request failed: ${resp.status} ${resp.statusText}`);
242
+ }
243
+ const body = await resp.json();
244
+ const messages = body.messages ?? body.data ?? body;
245
+ if (!Array.isArray(messages)) return [];
246
+ return messages.map(parseChatMessage);
247
+ }
248
+ /** @internal */
249
+ _onMessage(payload) {
250
+ const msgData = payload.message ?? payload;
251
+ if (msgData.type === "action" && msgData.action) {
252
+ const action = msgData.action;
253
+ const eventId = Number(action.event_id);
254
+ if (eventId && this._actionCallbacks.has(eventId)) {
255
+ this._actionCallbacks.get(eventId)(action);
256
+ }
257
+ }
258
+ const msg = parseChatMessage(msgData);
259
+ for (const h of this._messageHandlers) {
260
+ try {
261
+ const result = h(msg);
262
+ if (result && typeof result.catch === "function") {
263
+ result.catch(() => {
264
+ });
265
+ }
266
+ } catch {
267
+ }
268
+ }
269
+ }
270
+ /** @internal */
271
+ _onRead(payload) {
272
+ const receipt = {
273
+ topic_id: String(payload.topic_id ?? this._topicId ?? ""),
274
+ last_read_message_id: String(payload.last_read_message_id ?? ""),
275
+ read_at: Number(payload.read_at ?? 0)
276
+ };
277
+ for (const h of this._readHandlers) {
278
+ try {
279
+ const result = h(receipt);
280
+ if (result && typeof result.catch === "function") {
281
+ result.catch(() => {
282
+ });
283
+ }
284
+ } catch {
285
+ }
286
+ }
287
+ }
288
+ /** @internal */
289
+ _onSendAck(msg) {
290
+ const clientMsgId = String(msg.client_msg_id ?? "");
291
+ const pending = this._pendingSends.get(clientMsgId);
292
+ if (!pending) return;
293
+ clearTimeout(pending.timer);
294
+ this._pendingSends.delete(clientMsgId);
295
+ pending.resolve({
296
+ messageId: String(msg.message_id ?? ""),
297
+ sentAt: String(msg.sent_at ?? "")
298
+ });
299
+ }
300
+ /** @internal */
301
+ _onSendError(msg) {
302
+ const clientMsgId = String(msg.client_msg_id ?? "");
303
+ const pending = this._pendingSends.get(clientMsgId);
304
+ if (!pending) return;
305
+ clearTimeout(pending.timer);
306
+ this._pendingSends.delete(clientMsgId);
307
+ pending.reject(new ChatSendFailed(
308
+ Number(msg.status ?? 0),
309
+ String(msg.message ?? "")
310
+ ));
311
+ }
312
+ };
313
+ function parseChatMessage(data) {
314
+ const action = data.action;
315
+ return {
316
+ id: String(data.id ?? data._id ?? data.message_id ?? ""),
317
+ topic_id: String(data.topic_id ?? ""),
318
+ sender_id: data.sender_id != null ? Number(data.sender_id) : null,
319
+ text: data.text != null ? String(data.text) : null,
320
+ media: data.media ?? null,
321
+ type: String(data.type ?? "text"),
322
+ created_at: String(data.created_at ?? ""),
323
+ edited_at: data.edited_at != null ? String(data.edited_at) : null,
324
+ deleted_at: data.deleted_at != null ? String(data.deleted_at) : null,
325
+ action: action ? {
326
+ kind: String(action.kind ?? ""),
327
+ event_id: Number(action.event_id ?? 0),
328
+ data: action.data ?? void 0
329
+ } : null
330
+ };
331
+ }
332
+
333
+ // src/profile.ts
334
+ var BrowserProfile = class {
335
+ _browser;
336
+ constructor(browser) {
337
+ this._browser = browser;
338
+ }
339
+ async export(opts) {
340
+ let fingerprint = null;
341
+ try {
342
+ const fpResult = await this._browser.send({ method: "Browser.getFingerprint" });
343
+ fingerprint = fpResult ?? null;
344
+ } catch {
345
+ fingerprint = null;
346
+ }
347
+ let cookies = [];
348
+ try {
349
+ const cookieResult = await this._browser.send({ method: "Network.getCookies" });
350
+ const allCookies = cookieResult?.cookies ?? [];
351
+ if (opts?.domains && opts.domains.length > 0) {
352
+ const domainSet = new Set(opts.domains.map((d) => d.toLowerCase()));
353
+ cookies = allCookies.filter((c) => {
354
+ const cookieDomain = String(c.domain ?? "").toLowerCase().replace(/^\./, "");
355
+ return domainSet.has(cookieDomain) || opts.domains.some(
356
+ (d) => cookieDomain.endsWith("." + d.toLowerCase()) || cookieDomain === d.toLowerCase()
357
+ );
358
+ });
359
+ } else {
360
+ cookies = allCookies;
361
+ }
362
+ } catch {
363
+ cookies = [];
364
+ }
365
+ let localStorage = {};
366
+ try {
367
+ const lsResult = await this._browser.send({
368
+ method: "Runtime.evaluate",
369
+ params: { expression: "JSON.stringify(localStorage)", returnByValue: true }
370
+ });
371
+ const resultObj = lsResult?.result;
372
+ if (resultObj?.value) {
373
+ localStorage = JSON.parse(String(resultObj.value));
374
+ }
375
+ } catch {
376
+ localStorage = {};
377
+ }
378
+ let sessionStorage = {};
379
+ if (opts?.includeSessionStorage !== false) {
380
+ try {
381
+ const ssResult = await this._browser.send({
382
+ method: "Runtime.evaluate",
383
+ params: { expression: "JSON.stringify(sessionStorage)", returnByValue: true }
384
+ });
385
+ const resultObj = ssResult?.result;
386
+ if (resultObj?.value) {
387
+ sessionStorage = JSON.parse(String(resultObj.value));
388
+ }
389
+ } catch {
390
+ sessionStorage = {};
391
+ }
392
+ }
393
+ let origin = "";
394
+ try {
395
+ const originResult = await this._browser.send({
396
+ method: "Runtime.evaluate",
397
+ params: { expression: "location.origin", returnByValue: true }
398
+ });
399
+ const resultObj = originResult?.result;
400
+ if (resultObj?.value) {
401
+ origin = String(resultObj.value);
402
+ }
403
+ } catch {
404
+ origin = "";
405
+ }
406
+ return {
407
+ schema_version: 2,
408
+ fingerprint,
409
+ origin,
410
+ cookies,
411
+ localStorage,
412
+ sessionStorage
413
+ };
414
+ }
415
+ async import(profile) {
416
+ if (profile.schema_version !== 1 && profile.schema_version !== 2) {
417
+ throw new Error(`Unsupported profile schema_version: ${profile.schema_version}`);
418
+ }
419
+ if (profile.cookies && profile.cookies.length > 0) {
420
+ await this._browser.send({
421
+ method: "Network.setCookies",
422
+ params: { cookies: profile.cookies }
423
+ });
424
+ }
425
+ if (profile.localStorage && Object.keys(profile.localStorage).length > 0) {
426
+ const entries = JSON.stringify(profile.localStorage);
427
+ await this._browser.send({
428
+ method: "Runtime.evaluate",
429
+ params: {
430
+ expression: `(function(){var d=${entries};for(var k in d)localStorage.setItem(k,d[k])})()`,
431
+ returnByValue: true
432
+ }
433
+ });
434
+ }
435
+ if (profile.sessionStorage && Object.keys(profile.sessionStorage).length > 0) {
436
+ const entries = JSON.stringify(profile.sessionStorage);
437
+ await this._browser.send({
438
+ method: "Runtime.evaluate",
439
+ params: {
440
+ expression: `(function(){var d=${entries};for(var k in d)sessionStorage.setItem(k,d[k])})()`,
441
+ returnByValue: true
442
+ }
443
+ });
444
+ }
445
+ }
446
+ };
447
+
448
+ // src/state.ts
449
+ import * as fs2 from "fs";
450
+ import * as path2 from "path";
451
+ import * as os from "os";
452
+ var STATE_DIR = path2.join(os.homedir(), ".ceki", "sessions");
453
+ function statePath(sid) {
454
+ return path2.join(STATE_DIR, `${sid}.json`);
455
+ }
456
+ function loadSession(sid) {
457
+ try {
458
+ const raw = fs2.readFileSync(statePath(sid), "utf-8");
459
+ return JSON.parse(raw);
460
+ } catch {
461
+ return null;
462
+ }
463
+ }
464
+ function saveSession(sid, data) {
465
+ fs2.mkdirSync(STATE_DIR, { recursive: true });
466
+ const payload = { ...data, updated_at: (/* @__PURE__ */ new Date()).toISOString() };
467
+ fs2.writeFileSync(statePath(sid), JSON.stringify(payload, null, 2), "utf-8");
468
+ }
469
+ function deleteSession(sid) {
470
+ try {
471
+ fs2.unlinkSync(statePath(sid));
472
+ } catch (err2) {
473
+ if (err2.code !== "ENOENT") throw err2;
474
+ }
475
+ }
476
+ function getLastSeenTs(sid) {
477
+ const data = loadSession(sid);
478
+ if (!data) return null;
479
+ return data.last_seen_ts ?? null;
480
+ }
481
+ function updateLastSeenTs(sid, ts) {
482
+ const data = loadSession(sid) ?? {};
483
+ data.last_seen_ts = ts;
484
+ saveSession(sid, data);
485
+ }
486
+
487
+ // src/humanize/keymap.ts
488
+ var STATIC_MAP = {
489
+ " ": { code: "Space", key: " ", vk: 32, needsShift: false },
490
+ "\n": { code: "Enter", key: "Enter", vk: 13, needsShift: false },
491
+ " ": { code: "Tab", key: "Tab", vk: 9, needsShift: false },
492
+ // Shifted digits
493
+ "!": { code: "Digit1", key: "!", vk: 49, needsShift: true },
494
+ "@": { code: "Digit2", key: "@", vk: 50, needsShift: true },
495
+ "#": { code: "Digit3", key: "#", vk: 51, needsShift: true },
496
+ "$": { code: "Digit4", key: "$", vk: 52, needsShift: true },
497
+ "%": { code: "Digit5", key: "%", vk: 53, needsShift: true },
498
+ "^": { code: "Digit6", key: "^", vk: 54, needsShift: true },
499
+ "&": { code: "Digit7", key: "&", vk: 55, needsShift: true },
500
+ "*": { code: "Digit8", key: "*", vk: 56, needsShift: true },
501
+ "(": { code: "Digit9", key: "(", vk: 57, needsShift: true },
502
+ ")": { code: "Digit0", key: ")", vk: 48, needsShift: true },
503
+ // Punctuation (unshifted)
504
+ "-": { code: "Minus", key: "-", vk: 189, needsShift: false },
505
+ "=": { code: "Equal", key: "=", vk: 187, needsShift: false },
506
+ "[": { code: "BracketLeft", key: "[", vk: 219, needsShift: false },
507
+ "]": { code: "BracketRight", key: "]", vk: 221, needsShift: false },
508
+ "\\": { code: "Backslash", key: "\\", vk: 220, needsShift: false },
509
+ ";": { code: "Semicolon", key: ";", vk: 186, needsShift: false },
510
+ "'": { code: "Quote", key: "'", vk: 222, needsShift: false },
511
+ ",": { code: "Comma", key: ",", vk: 188, needsShift: false },
512
+ ".": { code: "Period", key: ".", vk: 190, needsShift: false },
513
+ "/": { code: "Slash", key: "/", vk: 191, needsShift: false },
514
+ "`": { code: "Backquote", key: "`", vk: 192, needsShift: false },
515
+ // Punctuation (shifted)
516
+ "_": { code: "Minus", key: "_", vk: 189, needsShift: true },
517
+ "+": { code: "Equal", key: "+", vk: 187, needsShift: true },
518
+ "{": { code: "BracketLeft", key: "{", vk: 219, needsShift: true },
519
+ "}": { code: "BracketRight", key: "}", vk: 221, needsShift: true },
520
+ "|": { code: "Backslash", key: "|", vk: 220, needsShift: true },
521
+ ":": { code: "Semicolon", key: ":", vk: 186, needsShift: true },
522
+ '"': { code: "Quote", key: '"', vk: 222, needsShift: true },
523
+ "<": { code: "Comma", key: "<", vk: 188, needsShift: true },
524
+ ">": { code: "Period", key: ">", vk: 190, needsShift: true },
525
+ "?": { code: "Slash", key: "?", vk: 191, needsShift: true },
526
+ "~": { code: "Backquote", key: "~", vk: 192, needsShift: true }
527
+ };
528
+ function keymapForChar(char) {
529
+ if (char >= "a" && char <= "z") {
530
+ const upper = char.toUpperCase();
531
+ return {
532
+ code: `Key${upper}`,
533
+ key: char,
534
+ vk: upper.charCodeAt(0),
535
+ needsShift: false
536
+ };
537
+ }
538
+ if (char >= "A" && char <= "Z") {
539
+ return {
540
+ code: `Key${char}`,
541
+ key: char,
542
+ vk: char.charCodeAt(0),
543
+ needsShift: true
544
+ };
545
+ }
546
+ if (char >= "0" && char <= "9") {
547
+ return {
548
+ code: `Digit${char}`,
549
+ key: char,
550
+ vk: char.charCodeAt(0),
551
+ needsShift: false
552
+ };
553
+ }
554
+ const mapping = STATIC_MAP[char];
555
+ if (mapping) return mapping;
556
+ return null;
557
+ }
558
+
559
+ // src/browser.ts
560
+ var Browser = class {
561
+ sessionId;
562
+ browserId;
563
+ scheduleId;
564
+ chatTopicId;
565
+ browserInfo;
566
+ providerUserId;
567
+ /** @internal */
568
+ _eventId;
569
+ chat;
570
+ profile;
571
+ /** @internal */
572
+ _client;
573
+ /** @internal */
574
+ _humanizer = null;
575
+ /** @internal */
576
+ _lastPointer = null;
577
+ /** @internal */
578
+ _lastSeenTs = null;
579
+ /** @internal */
580
+ _cdpCounter = 1;
581
+ /** @internal */
582
+ _pendingCdp = /* @__PURE__ */ new Map();
583
+ /** @internal */
584
+ _ended;
585
+ /** @internal */
586
+ _endedReason = null;
587
+ /** @internal */
588
+ _resolveEnded;
589
+ _eventHandlers = [];
590
+ _tabHandlers = [];
591
+ _disconnectHandlers = [];
592
+ _reconnectHandlers = [];
593
+ _userEventHandlers = [];
594
+ /** @internal */
595
+ get _apiKey() {
596
+ return this._client._apiKey;
597
+ }
598
+ /** @internal */
599
+ get _chatUrl() {
600
+ return this._client._chatUrl;
601
+ }
602
+ /** @internal */
603
+ get _basicAuth() {
604
+ return this._client._basicAuth;
605
+ }
606
+ constructor(client, match, humanizer) {
607
+ this._client = client;
608
+ this.sessionId = match.session_id;
609
+ this.browserId = match.schedule_id;
610
+ this.scheduleId = match.schedule_id;
611
+ this.chatTopicId = match.chat_topic_id ?? null;
612
+ this.browserInfo = match.browser_info ?? {};
613
+ this.providerUserId = match.provider_user_id ?? null;
614
+ this._eventId = match.event_id ?? null;
615
+ this._humanizer = humanizer ?? null;
616
+ this.chat = new BrowserChat(this);
617
+ this.profile = new BrowserProfile(this);
618
+ this._ended = new Promise((resolve2) => {
619
+ this._resolveEnded = resolve2;
620
+ });
621
+ this._lastSeenTs = getLastSeenTs(this.sessionId);
622
+ saveSession(this.sessionId, {
623
+ session_id: this.sessionId,
624
+ chat_topic_id: this.chatTopicId,
625
+ schedule_id: this.scheduleId,
626
+ last_seen_ts: this._lastSeenTs
627
+ });
628
+ }
629
+ /** @internal — send raw message via client WS */
630
+ _sendRaw(msg) {
631
+ this._client._wsSend(msg);
632
+ }
633
+ async send(cdp, timeout = 3e4) {
634
+ if (this._endedReason) {
635
+ throw new SessionEnded(this._endedReason);
636
+ }
637
+ const id = this._cdpCounter++;
638
+ const msg = {
639
+ type: "cdp",
640
+ session_id: this.sessionId,
641
+ id,
642
+ method: cdp.method,
643
+ params: cdp.params ?? {}
644
+ };
645
+ return new Promise((resolve2, reject) => {
646
+ const timer = setTimeout(() => {
647
+ this._pendingCdp.delete(id);
648
+ reject(new TimeoutError(`CDP ${cdp.method} timed out after ${timeout}ms`));
649
+ }, timeout);
650
+ this._pendingCdp.set(id, { resolve: resolve2, reject, timer });
651
+ this._sendRaw(msg);
652
+ });
653
+ }
654
+ async navigate(url, timeout = 3e4) {
655
+ if (this._humanizer) await this._humanizer.before("navigate");
656
+ const result = await this.send({ method: "Page.navigate", params: { url } }, timeout);
657
+ if (this._humanizer) await this._humanizer.after("navigate");
658
+ return {
659
+ url: String(result?.url ?? url),
660
+ frameId: result?.frameId ? String(result.frameId) : void 0
661
+ };
662
+ }
663
+ async click(x, y) {
664
+ if (this._humanizer) await this._humanizer.before("click");
665
+ await this.send({
666
+ method: "Input.dispatchMouseEvent",
667
+ params: { type: "mousePressed", x, y, button: "left", clickCount: 1 }
668
+ });
669
+ await this.send({
670
+ method: "Input.dispatchMouseEvent",
671
+ params: { type: "mouseReleased", x, y, button: "left", clickCount: 1 }
672
+ });
673
+ this._lastPointer = [x, y];
674
+ if (this._humanizer) await this._humanizer.after("click");
675
+ }
676
+ async _sendKeystroke(char) {
677
+ const mapping = keymapForChar(char);
678
+ if (!mapping) {
679
+ await this.send({ method: "Input.insertText", params: { text: char } });
680
+ return;
681
+ }
682
+ const { code, key, vk, needsShift } = mapping;
683
+ if (needsShift) {
684
+ await this.send({
685
+ method: "Input.dispatchKeyEvent",
686
+ params: { type: "keyDown", key: "Shift", code: "ShiftLeft", windowsVirtualKeyCode: 16, nativeVirtualKeyCode: 16 }
687
+ });
688
+ }
689
+ await this.send({
690
+ method: "Input.dispatchKeyEvent",
691
+ params: {
692
+ type: "keyDown",
693
+ key,
694
+ code,
695
+ text: char,
696
+ unmodifiedText: needsShift ? char.toLowerCase() : char,
697
+ windowsVirtualKeyCode: vk,
698
+ nativeVirtualKeyCode: vk,
699
+ ...needsShift ? { modifiers: 8 } : {}
700
+ }
701
+ });
702
+ await this.send({
703
+ method: "Input.dispatchKeyEvent",
704
+ params: {
705
+ type: "keyUp",
706
+ key,
707
+ code,
708
+ windowsVirtualKeyCode: vk,
709
+ nativeVirtualKeyCode: vk,
710
+ ...needsShift ? { modifiers: 8 } : {}
711
+ }
712
+ });
713
+ if (needsShift) {
714
+ await this.send({
715
+ method: "Input.dispatchKeyEvent",
716
+ params: { type: "keyUp", key: "Shift", code: "ShiftLeft", windowsVirtualKeyCode: 16, nativeVirtualKeyCode: 16 }
717
+ });
718
+ }
719
+ }
720
+ async type(text) {
721
+ if (this._humanizer) {
722
+ await this._humanizer.before("type");
723
+ if (this._lastPointer) {
724
+ const [px, py] = this._lastPointer;
725
+ await this.send({
726
+ method: "Input.dispatchMouseEvent",
727
+ params: { type: "mousePressed", x: px, y: py, button: "left", clickCount: 1 }
728
+ });
729
+ await this.send({
730
+ method: "Input.dispatchMouseEvent",
731
+ params: { type: "mouseReleased", x: px, y: py, button: "left", clickCount: 1 }
732
+ });
733
+ }
734
+ for (const char of text) {
735
+ await this._sendKeystroke(char);
736
+ const delay = this._humanizer.typeDelay();
737
+ if (delay > 0) {
738
+ await new Promise((r) => setTimeout(r, delay));
739
+ }
740
+ }
741
+ await this._humanizer.after("type");
742
+ } else {
743
+ for (const char of text) {
744
+ await this._sendKeystroke(char);
745
+ }
746
+ }
747
+ }
748
+ async scroll(opts) {
749
+ const x = opts?.x ?? 0;
750
+ const y = opts?.y ?? 0;
751
+ const deltaX = opts?.deltaX ?? 0;
752
+ const deltaY = opts?.deltaY ?? -300;
753
+ if (this._humanizer) await this._humanizer.before("scroll");
754
+ await this.send({
755
+ method: "Input.dispatchMouseEvent",
756
+ params: { type: "mouseWheel", x, y, deltaX, deltaY }
757
+ });
758
+ this._lastPointer = [x, y];
759
+ if (this._humanizer) await this._humanizer.after("scroll");
760
+ }
761
+ async screenshot(opts) {
762
+ const format = opts?.format ?? "base64";
763
+ const fullPage = opts?.fullPage ?? false;
764
+ let clip;
765
+ if (fullPage) {
766
+ const metrics = await this.send({ method: "Page.getLayoutMetrics" });
767
+ const contentSize = metrics?.contentSize;
768
+ if (contentSize) {
769
+ const width = Number(contentSize.width ?? 1920);
770
+ const height = Math.min(Number(contentSize.height ?? 1080), 16384);
771
+ clip = { x: 0, y: 0, width, height, scale: 1 };
772
+ }
773
+ }
774
+ const params = { format: "png" };
775
+ if (clip) params.clip = clip;
776
+ const result = await this.send({ method: "Page.captureScreenshot", params });
777
+ const data = String(result?.data ?? "");
778
+ if (format === "png") {
779
+ return Buffer.from(data, "base64");
780
+ }
781
+ return { data };
782
+ }
783
+ async snapshot() {
784
+ const [ssResult, chatMessages] = await Promise.all([
785
+ this.screenshot({ format: "base64" }),
786
+ this.chat.history({ since: this._lastSeenTs ?? void 0 })
787
+ ]);
788
+ const screenshotData = ssResult.data;
789
+ if (chatMessages.length > 0) {
790
+ const lastMsg = chatMessages[chatMessages.length - 1];
791
+ this._lastSeenTs = lastMsg.created_at;
792
+ updateLastSeenTs(this.sessionId, this._lastSeenTs);
793
+ }
794
+ return {
795
+ screenshot: screenshotData,
796
+ chat: chatMessages,
797
+ ts: /* @__PURE__ */ new Date()
798
+ };
799
+ }
800
+ async upload(selector, source, filename) {
801
+ let buf;
802
+ let resolvedFilename;
803
+ if (typeof source === "string") {
804
+ const fs5 = await import("fs");
805
+ const path4 = await import("path");
806
+ buf = fs5.readFileSync(source);
807
+ resolvedFilename = filename ?? path4.basename(source);
808
+ } else {
809
+ buf = Buffer.isBuffer(source) ? source : Buffer.from(source);
810
+ resolvedFilename = filename ?? "file";
811
+ }
812
+ const b64 = buf.toString("base64");
813
+ const size = buf.length;
814
+ const expression = `
815
+ (function() {
816
+ var input = document.querySelector(${JSON.stringify(selector)});
817
+ if (!input) return JSON.stringify({ok: false, error: 'Element not found'});
818
+ var b64 = ${JSON.stringify(b64)};
819
+ var binary = atob(b64);
820
+ var bytes = new Uint8Array(binary.length);
821
+ for (var i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
822
+ var file = new File([bytes], ${JSON.stringify(resolvedFilename)}, {type: 'application/octet-stream'});
823
+ var dt = new DataTransfer();
824
+ dt.items.add(file);
825
+ input.files = dt.files;
826
+ input.dispatchEvent(new Event('change', {bubbles: true}));
827
+ return JSON.stringify({ok: true, filename: ${JSON.stringify(resolvedFilename)}, size: ${size}});
828
+ })()
829
+ `.trim();
830
+ const result = await this.send({
831
+ method: "Runtime.evaluate",
832
+ params: { expression, returnByValue: true }
833
+ });
834
+ const resultObj = result?.result;
835
+ if (resultObj?.value) {
836
+ return JSON.parse(String(resultObj.value));
837
+ }
838
+ return { ok: true, filename: resolvedFilename, size };
839
+ }
840
+ async switchTab() {
841
+ this._sendRaw({ type: "switch_tab", session_id: this.sessionId });
842
+ }
843
+ async configure(opts) {
844
+ const msg = {
845
+ type: "session.configure",
846
+ session_id: this.sessionId
847
+ };
848
+ if (opts.maskingMode !== void 0) msg.masking_mode = opts.maskingMode;
849
+ if (opts.fingerprint !== void 0) msg.fingerprint = opts.fingerprint;
850
+ this._sendRaw(msg);
851
+ }
852
+ async close(timeout = 1e4) {
853
+ if (this._endedReason) return;
854
+ this._sendRaw({
855
+ type: "session.end",
856
+ session_id: this.sessionId,
857
+ reason: "user_stop"
858
+ });
859
+ await Promise.race([
860
+ this._ended,
861
+ new Promise(
862
+ (_, reject) => setTimeout(() => reject(new TimeoutError("Close timed out")), timeout)
863
+ )
864
+ ]).catch(() => {
865
+ });
866
+ this._cleanup();
867
+ }
868
+ async release(timeout) {
869
+ return this.close(timeout);
870
+ }
871
+ async waitUntilEnded() {
872
+ return this._ended;
873
+ }
874
+ onEvent(cb) {
875
+ this._eventHandlers.push(cb);
876
+ }
877
+ onTabOpened(cb) {
878
+ this._tabHandlers.push(cb);
879
+ }
880
+ onProviderDisconnected(cb) {
881
+ this._disconnectHandlers.push(cb);
882
+ }
883
+ onProviderReconnected(cb) {
884
+ this._reconnectHandlers.push(cb);
885
+ }
886
+ onUserEvent(cb) {
887
+ this._userEventHandlers.push(cb);
888
+ }
889
+ // --- Captcha / human action ---
890
+ _apiHeaders() {
891
+ const headers = {
892
+ "Authorization": `Bearer ${this._apiKey}`,
893
+ "Content-Type": "application/json"
894
+ };
895
+ const basicAuth = this._basicAuth;
896
+ if (basicAuth) {
897
+ const encoded = Buffer.from(`${basicAuth[0]}:${basicAuth[1]}`).toString("base64");
898
+ headers["X-Basic-Auth"] = `Basic ${encoded}`;
899
+ }
900
+ return headers;
901
+ }
902
+ async requestCaptcha(opts) {
903
+ let acceptanceTimeout = opts?.acceptanceTimeout ?? 60;
904
+ let completionTimeout = opts?.completionTimeout ?? 120;
905
+ const autoAccept = opts?.autoAccept ?? true;
906
+ if (acceptanceTimeout < 30) throw new Error("acceptanceTimeout must be >= 30 seconds");
907
+ if (completionTimeout < 30) throw new Error("completionTimeout must be >= 30 seconds");
908
+ acceptanceTimeout = Math.min(acceptanceTimeout, 300);
909
+ completionTimeout = Math.min(completionTimeout, 600);
910
+ const { id: childEventId } = await this._createCaptchaEvent(acceptanceTimeout, completionTimeout);
911
+ const completionDeadline = Date.now() + completionTimeout * 1e3;
912
+ const buffer = [];
913
+ let waiter = null;
914
+ this.chat._actionCallbacks.set(childEventId, (action) => {
915
+ if (waiter) {
916
+ const w = waiter;
917
+ waiter = null;
918
+ w(action);
919
+ } else {
920
+ buffer.push(action);
921
+ }
922
+ });
923
+ const nextAction = (timeoutMs) => {
924
+ if (buffer.length > 0) return Promise.resolve(buffer.shift());
925
+ return new Promise((resolve2, reject) => {
926
+ const timer = setTimeout(() => {
927
+ waiter = null;
928
+ reject(new Error("timeout"));
929
+ }, timeoutMs);
930
+ waiter = (action) => {
931
+ clearTimeout(timer);
932
+ resolve2(action);
933
+ };
934
+ });
935
+ };
936
+ const cleanup = () => {
937
+ this.chat._actionCallbacks.delete(childEventId);
938
+ waiter = null;
939
+ };
940
+ const makeResult = async (data, solved) => {
941
+ const correctionId = data.correction_id != null ? Number(data.correction_id) : null;
942
+ const proofMessageId = data.proof_message_id != null ? String(data.proof_message_id) : null;
943
+ let voted = false;
944
+ const result = {
945
+ solved,
946
+ proofMessageId,
947
+ cancelReason: null,
948
+ childEventId,
949
+ correctionId,
950
+ acceptWork: async () => {
951
+ if (voted) return;
952
+ if (!correctionId) throw new CaptchaError("no correction_id \u2014 provider has not proposed completion");
953
+ voted = true;
954
+ await fetch(`${this._client._apiUrl}/api/agent/kal/event/${childEventId}/vote`, {
955
+ method: "POST",
956
+ headers: this._apiHeaders(),
957
+ body: JSON.stringify({ ids: [correctionId], vote: true })
958
+ });
959
+ },
960
+ rejectWork: async (reason) => {
961
+ if (voted) return;
962
+ if (!correctionId) throw new CaptchaError("no correction_id \u2014 provider has not proposed completion");
963
+ voted = true;
964
+ const body = { ids: [correctionId], vote: false };
965
+ if (reason) body.reason = reason;
966
+ await fetch(`${this._client._apiUrl}/api/agent/kal/event/${childEventId}/vote`, {
967
+ method: "POST",
968
+ headers: this._apiHeaders(),
969
+ body: JSON.stringify(body)
970
+ });
971
+ }
972
+ };
973
+ if (autoAccept && solved && correctionId) {
974
+ await new Promise((r) => setTimeout(r, 2e3));
975
+ await result.acceptWork();
976
+ }
977
+ return result;
978
+ };
979
+ let accepted = false;
980
+ try {
981
+ const acceptDeadline = Date.now() + acceptanceTimeout * 1e3;
982
+ while (true) {
983
+ const remaining = acceptDeadline - Date.now();
984
+ if (remaining <= 0) throw new Error("timeout");
985
+ const action = await nextAction(remaining);
986
+ const kind = String(action.kind ?? "");
987
+ const data = action.data ?? {};
988
+ if (kind === "human_action_accepted") {
989
+ accepted = true;
990
+ break;
991
+ }
992
+ if (kind === "human_action_completed") {
993
+ cleanup();
994
+ return await makeResult(data, true);
995
+ }
996
+ if (kind === "human_action_failed" || kind === "human_action_declined" || kind === "human_action_withdrew") {
997
+ cleanup();
998
+ return {
999
+ solved: false,
1000
+ proofMessageId: null,
1001
+ cancelReason: kind.replace("human_action_", ""),
1002
+ childEventId,
1003
+ correctionId: null,
1004
+ acceptWork: async () => {
1005
+ },
1006
+ rejectWork: async () => {
1007
+ }
1008
+ };
1009
+ }
1010
+ }
1011
+ while (true) {
1012
+ const remaining = completionDeadline - Date.now();
1013
+ if (remaining <= 0) throw new Error("timeout");
1014
+ const action = await nextAction(remaining);
1015
+ const kind = String(action.kind ?? "");
1016
+ const data = action.data ?? {};
1017
+ if (kind === "human_action_completed") {
1018
+ cleanup();
1019
+ return await makeResult(data, true);
1020
+ }
1021
+ if (kind === "human_action_failed" || kind === "human_action_withdrew") {
1022
+ cleanup();
1023
+ return {
1024
+ solved: false,
1025
+ proofMessageId: null,
1026
+ cancelReason: kind.replace("human_action_", ""),
1027
+ childEventId,
1028
+ correctionId: null,
1029
+ acceptWork: async () => {
1030
+ },
1031
+ rejectWork: async () => {
1032
+ }
1033
+ };
1034
+ }
1035
+ }
1036
+ } catch {
1037
+ cleanup();
1038
+ const phase = accepted ? "completion" : "acceptance";
1039
+ await this._expireCaptchaEvent(childEventId);
1040
+ throw new CaptchaTimeoutError(phase);
1041
+ }
1042
+ }
1043
+ async _createCaptchaEvent(acceptanceTimeout, completionTimeout) {
1044
+ const body = {
1045
+ acceptance_deadline_at: Math.floor(acceptanceTimeout),
1046
+ completion_deadline_at: Math.floor(completionTimeout)
1047
+ };
1048
+ const resp = await fetch(`${this._client._apiUrl}/api/agent/sessions/${this._eventId}/captcha-request`, {
1049
+ method: "POST",
1050
+ headers: this._apiHeaders(),
1051
+ body: JSON.stringify(body)
1052
+ });
1053
+ if (!resp.ok) {
1054
+ throw new Error(`Captcha request failed: ${resp.status}`);
1055
+ }
1056
+ const result = await resp.json();
1057
+ if (!result.id) throw new Error("Captcha request did not return an id");
1058
+ return { id: result.id, amount: result.amount };
1059
+ }
1060
+ async _expireCaptchaEvent(childEventId) {
1061
+ try {
1062
+ await fetch(`${this._client._apiUrl}/api/agent/kal/event/${childEventId}`, {
1063
+ method: "PATCH",
1064
+ headers: this._apiHeaders(),
1065
+ body: JSON.stringify({ status_id: 777 })
1066
+ });
1067
+ } catch {
1068
+ }
1069
+ }
1070
+ // --- Internal handlers called by Client dispatch ---
1071
+ /** @internal */
1072
+ _onCdpResponse(msg) {
1073
+ const id = Number(msg.id);
1074
+ const pending = this._pendingCdp.get(id);
1075
+ if (!pending) return;
1076
+ clearTimeout(pending.timer);
1077
+ this._pendingCdp.delete(id);
1078
+ if (msg.ok === false) {
1079
+ const error = msg.error;
1080
+ pending.reject(new Error(String(error?.message ?? error?.code ?? "CDP error")));
1081
+ } else {
1082
+ pending.resolve(msg.result ?? null);
1083
+ }
1084
+ }
1085
+ /** @internal */
1086
+ _onCdpEvent(msg) {
1087
+ const method = String(msg.method ?? "");
1088
+ const params = msg.params ?? {};
1089
+ for (const h of this._eventHandlers) {
1090
+ try {
1091
+ h(method, params);
1092
+ } catch {
1093
+ }
1094
+ }
1095
+ }
1096
+ /** @internal */
1097
+ _onTabOpened(msg) {
1098
+ const url = String(msg.url ?? "");
1099
+ for (const h of this._tabHandlers) {
1100
+ try {
1101
+ h(url);
1102
+ } catch {
1103
+ }
1104
+ }
1105
+ }
1106
+ /** @internal */
1107
+ _onSessionEnded(msg) {
1108
+ const reason = String(msg.reason ?? "unknown");
1109
+ this._endedReason = reason;
1110
+ this._resolveEnded(reason);
1111
+ this._rejectAllPending(new SessionEnded(reason));
1112
+ this._cleanup();
1113
+ }
1114
+ /** @internal */
1115
+ _onProviderDisconnected() {
1116
+ for (const h of this._disconnectHandlers) {
1117
+ try {
1118
+ h();
1119
+ } catch {
1120
+ }
1121
+ }
1122
+ }
1123
+ /** @internal */
1124
+ _onProviderReconnected() {
1125
+ for (const h of this._reconnectHandlers) {
1126
+ try {
1127
+ h();
1128
+ } catch {
1129
+ }
1130
+ }
1131
+ }
1132
+ /** @internal */
1133
+ _onError(msg) {
1134
+ const reason = String(msg.reason ?? msg.message ?? "unknown error");
1135
+ this._endedReason = reason;
1136
+ this._resolveEnded(reason);
1137
+ this._rejectAllPending(new SessionEnded(reason));
1138
+ this._cleanup();
1139
+ }
1140
+ /** @internal */
1141
+ _onUserEvents(msg) {
1142
+ const events = msg.events ?? [];
1143
+ for (const h of this._userEventHandlers) {
1144
+ try {
1145
+ h(events);
1146
+ } catch {
1147
+ }
1148
+ }
1149
+ }
1150
+ /** @internal */
1151
+ _onChatMessage(payload) {
1152
+ this.chat._onMessage(payload);
1153
+ }
1154
+ /** @internal */
1155
+ _onChatRead(payload) {
1156
+ this.chat._onRead(payload);
1157
+ }
1158
+ /** @internal */
1159
+ _onChatSendAck(msg) {
1160
+ this.chat._onSendAck(msg);
1161
+ }
1162
+ /** @internal */
1163
+ _onChatSendError(msg) {
1164
+ this.chat._onSendError(msg);
1165
+ }
1166
+ _rejectAllPending(err2) {
1167
+ for (const [id, pending] of this._pendingCdp) {
1168
+ clearTimeout(pending.timer);
1169
+ pending.reject(err2);
1170
+ }
1171
+ this._pendingCdp.clear();
1172
+ }
1173
+ _cleanup() {
1174
+ this._client._activeBrowsers.delete(this.sessionId);
1175
+ }
1176
+ };
1177
+
1178
+ // src/humanize/humanizer.ts
1179
+ function seededRandom(seed) {
1180
+ if (seed === null || seed === void 0) {
1181
+ return Math.random;
1182
+ }
1183
+ let s = seed;
1184
+ return () => {
1185
+ s = s * 1664525 + 1013904223 & 4294967295;
1186
+ return (s >>> 0) / 4294967295;
1187
+ };
1188
+ }
1189
+ function gaussianRandom(rng, mean, sigma) {
1190
+ let u1, u2;
1191
+ do {
1192
+ u1 = rng();
1193
+ } while (u1 === 0);
1194
+ u2 = rng();
1195
+ const z = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
1196
+ return mean + z * sigma;
1197
+ }
1198
+ var Humanizer = class {
1199
+ profile;
1200
+ _rng;
1201
+ constructor(profile) {
1202
+ this.profile = profile;
1203
+ this._rng = seededRandom(profile.raw.rng_seed ?? null);
1204
+ }
1205
+ async before(action) {
1206
+ const [lo, hi] = this.profile.getRange(action, "pre");
1207
+ if (lo === 0 && hi === 0) return;
1208
+ const delay = lo + this._rng() * (hi - lo);
1209
+ await sleep(delay);
1210
+ }
1211
+ async after(action) {
1212
+ const [lo, hi] = this.profile.getRange(action, "post");
1213
+ if (lo === 0 && hi === 0) return;
1214
+ const delay = lo + this._rng() * (hi - lo);
1215
+ await sleep(delay);
1216
+ }
1217
+ typeDelay() {
1218
+ const typing = this.profile.raw.typing ?? {};
1219
+ const wpm = typing.wpm ?? 110;
1220
+ const jitter = typing.jitter ?? 0.35;
1221
+ const thinkProb = typing.thinking_pause_prob ?? 0;
1222
+ const thinkMs = typing.thinking_pause_ms ?? [300, 1200];
1223
+ const meanInterval = 6e4 / (wpm * 5);
1224
+ const sigma = meanInterval * jitter;
1225
+ let delay = gaussianRandom(this._rng, meanInterval, sigma);
1226
+ delay = Math.max(delay, 20);
1227
+ if (thinkProb > 0 && this._rng() < thinkProb) {
1228
+ delay += thinkMs[0] + this._rng() * (thinkMs[1] - thinkMs[0]);
1229
+ }
1230
+ return delay;
1231
+ }
1232
+ };
1233
+ function sleep(ms) {
1234
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
1235
+ }
1236
+
1237
+ // src/humanize/profile.ts
1238
+ import * as fs3 from "fs";
1239
+ var PRESETS = {
1240
+ natural: {
1241
+ version: 1,
1242
+ name: "natural",
1243
+ typing: { wpm: 110, jitter: 0.35, thinking_pause_prob: 0.012, thinking_pause_ms: [300, 1200], typo_prob: 0 },
1244
+ pre_action_ms: { click: [80, 350], type: [120, 500], scroll: [50, 250], navigate: [0, 0], screenshot: [0, 0] },
1245
+ post_action_ms: { click: [150, 800], type: [150, 800], scroll: [200, 900], navigate: [400, 1800], screenshot: [0, 0] },
1246
+ mouse: { move_before_click: false, trajectory: "off" },
1247
+ rng_seed: null
1248
+ },
1249
+ careful: {
1250
+ version: 1,
1251
+ name: "careful",
1252
+ typing: { wpm: 80, jitter: 0.4, thinking_pause_prob: 0.025, thinking_pause_ms: [400, 1800], typo_prob: 0 },
1253
+ pre_action_ms: { click: [200, 600], type: [250, 800], scroll: [100, 400], navigate: [0, 0], screenshot: [0, 0] },
1254
+ post_action_ms: { click: [400, 1500], type: [300, 1200], scroll: [300, 1200], navigate: [800, 3e3], screenshot: [0, 0] },
1255
+ mouse: { move_before_click: false, trajectory: "off" },
1256
+ rng_seed: null
1257
+ }
1258
+ };
1259
+ var DEFAULTS = {
1260
+ version: 1,
1261
+ name: "custom",
1262
+ typing: {
1263
+ wpm: 110,
1264
+ jitter: 0.35,
1265
+ thinking_pause_prob: 0.012,
1266
+ thinking_pause_ms: [300, 1200],
1267
+ typo_prob: 0
1268
+ },
1269
+ pre_action_ms: {
1270
+ click: [80, 350],
1271
+ type: [120, 500],
1272
+ scroll: [50, 250],
1273
+ navigate: [0, 0],
1274
+ screenshot: [0, 0]
1275
+ },
1276
+ post_action_ms: {
1277
+ click: [150, 800],
1278
+ type: [150, 800],
1279
+ scroll: [200, 900],
1280
+ navigate: [400, 1800],
1281
+ screenshot: [0, 0]
1282
+ },
1283
+ mouse: {
1284
+ move_before_click: false,
1285
+ trajectory: "off"
1286
+ },
1287
+ rng_seed: null
1288
+ };
1289
+ function deepMerge(base, override) {
1290
+ const result = { ...base };
1291
+ for (const [key, value] of Object.entries(override)) {
1292
+ if (key in result && typeof result[key] === "object" && result[key] !== null && !Array.isArray(result[key]) && typeof value === "object" && value !== null && !Array.isArray(value)) {
1293
+ result[key] = deepMerge(
1294
+ result[key],
1295
+ value
1296
+ );
1297
+ } else {
1298
+ result[key] = value;
1299
+ }
1300
+ }
1301
+ return result;
1302
+ }
1303
+ var HumanProfile = class _HumanProfile {
1304
+ name;
1305
+ raw;
1306
+ constructor(name, raw) {
1307
+ this.name = name;
1308
+ this.raw = raw;
1309
+ }
1310
+ static fromDict(d) {
1311
+ const merged = deepMerge(
1312
+ DEFAULTS,
1313
+ d
1314
+ );
1315
+ const name = d.name ?? "custom";
1316
+ merged.name = name;
1317
+ return new _HumanProfile(name, merged);
1318
+ }
1319
+ static load(filePath) {
1320
+ const raw = fs3.readFileSync(filePath, "utf-8");
1321
+ const data = JSON.parse(raw);
1322
+ return _HumanProfile.fromDict(data);
1323
+ }
1324
+ static loadPreset(name) {
1325
+ const preset = PRESETS[name];
1326
+ if (!preset) {
1327
+ throw new Error(`Preset '${name}' not found. Available: ${Object.keys(PRESETS).join(", ")}`);
1328
+ }
1329
+ return _HumanProfile.fromDict(preset);
1330
+ }
1331
+ getRange(action, phase) {
1332
+ const key = `${phase}_action_ms`;
1333
+ const mapping = this.raw[key];
1334
+ const pair = mapping?.[action];
1335
+ if (Array.isArray(pair) && pair.length === 2) {
1336
+ return [pair[0], pair[1]];
1337
+ }
1338
+ return [0, 0];
1339
+ }
1340
+ typingInterval() {
1341
+ const wpm = this.raw.typing?.wpm ?? 110;
1342
+ return 6e4 / (wpm * 5);
1343
+ }
1344
+ toDict() {
1345
+ return JSON.parse(JSON.stringify(this.raw));
1346
+ }
1347
+ toJSON(indent = 2) {
1348
+ return JSON.stringify(this.raw, null, indent);
1349
+ }
1350
+ };
1351
+
1352
+ // src/client.ts
1353
+ var BACKOFF_SCHEDULE = [1, 2, 4, 8, 16, 32, 60];
1354
+ var MAX_RECONNECT_ATTEMPTS = 10;
1355
+ var PING_INTERVAL = 3e4;
1356
+ var PONG_TIMEOUT = 9e4;
1357
+ var Client = class _Client {
1358
+ /** @internal */
1359
+ _apiKey;
1360
+ /** @internal */
1361
+ _chatUrl;
1362
+ /** @internal */
1363
+ _basicAuth;
1364
+ /** @internal */
1365
+ _activeBrowsers = /* @__PURE__ */ new Map();
1366
+ _ws = null;
1367
+ /** @internal */
1368
+ _apiUrl;
1369
+ _relayUrl;
1370
+ _reconnect;
1371
+ _reconnectAttempt = 0;
1372
+ _reconnecting = false;
1373
+ _closed = false;
1374
+ _pingTimer = null;
1375
+ _pongTimer = null;
1376
+ _lastPongAt = 0;
1377
+ _pendingRents = /* @__PURE__ */ new Map();
1378
+ // keyed by `rent:<scheduleId>` or eventId
1379
+ _pendingResumes = /* @__PURE__ */ new Map();
1380
+ // keyed by sessionId
1381
+ _connectResolve = null;
1382
+ _connectReject = null;
1383
+ constructor(apiKey, opts) {
1384
+ const cfg = resolveConfig(opts);
1385
+ this._apiKey = apiKey;
1386
+ this._apiUrl = cfg.apiUrl;
1387
+ this._relayUrl = cfg.relayUrl;
1388
+ this._chatUrl = cfg.chatUrl;
1389
+ this._basicAuth = cfg.basicAuth;
1390
+ this._reconnect = cfg.reconnect;
1391
+ }
1392
+ /** Factory: create client and connect */
1393
+ static async create(apiKey, opts) {
1394
+ const client = new _Client(apiKey, opts);
1395
+ await client._connect();
1396
+ return client;
1397
+ }
1398
+ /** @internal */
1399
+ _wsSend(msg) {
1400
+ if (!this._ws || this._ws.readyState !== WebSocket.OPEN) {
1401
+ throw new ConnectionLost("WebSocket not connected");
1402
+ }
1403
+ this._ws.send(JSON.stringify(msg));
1404
+ }
1405
+ async search(filters, limit) {
1406
+ const params = new URLSearchParams();
1407
+ if (limit != null) params.set("limit", String(limit));
1408
+ if (filters) {
1409
+ for (const [key, value] of Object.entries(filters)) {
1410
+ if (value != null) params.set(key, String(value));
1411
+ }
1412
+ }
1413
+ const url = `${this._apiUrl}/api/browsers/search?${params.toString()}`;
1414
+ const headers = {
1415
+ "Authorization": `Bearer ${this._apiKey}`
1416
+ };
1417
+ if (this._basicAuth) {
1418
+ const encoded = Buffer.from(`${this._basicAuth[0]}:${this._basicAuth[1]}`).toString("base64");
1419
+ headers["X-Basic-Auth"] = `Basic ${encoded}`;
1420
+ }
1421
+ const resp = await fetch(url, { headers });
1422
+ if (!resp.ok) {
1423
+ throw new TransportError(`Search request failed: ${resp.status} ${resp.statusText}`);
1424
+ }
1425
+ const body = await resp.json();
1426
+ const data = body.data ?? body;
1427
+ return Array.isArray(data) ? data : [];
1428
+ }
1429
+ async listSessions(opts) {
1430
+ const active = opts?.active ?? true;
1431
+ const limit = opts?.limit ?? 50;
1432
+ const params = new URLSearchParams({
1433
+ active: active ? "1" : "0",
1434
+ limit: String(limit)
1435
+ });
1436
+ const url = `${this._apiUrl}/api/agent/sessions?${params.toString()}`;
1437
+ const headers = {
1438
+ "Authorization": `Bearer ${this._apiKey}`
1439
+ };
1440
+ if (this._basicAuth) {
1441
+ const encoded = Buffer.from(`${this._basicAuth[0]}:${this._basicAuth[1]}`).toString("base64");
1442
+ headers["X-Basic-Auth"] = `Basic ${encoded}`;
1443
+ }
1444
+ const resp = await fetch(url, { headers });
1445
+ if (!resp.ok) {
1446
+ throw new TransportError(`listSessions request failed: ${resp.status} ${resp.statusText}`);
1447
+ }
1448
+ const body = await resp.json();
1449
+ const items = body.data ?? body;
1450
+ return Array.isArray(items) ? items : [];
1451
+ }
1452
+ async myBrowsers() {
1453
+ const url = `${this._apiUrl}/api/agent/browsers`;
1454
+ const headers = {
1455
+ "Authorization": `Bearer ${this._apiKey}`
1456
+ };
1457
+ if (this._basicAuth) {
1458
+ const encoded = Buffer.from(`${this._basicAuth[0]}:${this._basicAuth[1]}`).toString("base64");
1459
+ headers["X-Basic-Auth"] = `Basic ${encoded}`;
1460
+ }
1461
+ const resp = await fetch(url, { headers });
1462
+ if (!resp.ok) {
1463
+ throw new TransportError(`myBrowsers request failed: ${resp.status} ${resp.statusText}`);
1464
+ }
1465
+ const body = await resp.json();
1466
+ const items = body.browsers ?? body.data ?? body;
1467
+ return Array.isArray(items) ? items : [];
1468
+ }
1469
+ async rent(scheduleId, opts) {
1470
+ const rentMsg = { type: "rent", browser_id: scheduleId };
1471
+ if (opts?.mode) rentMsg.mode = opts.mode;
1472
+ this._wsSend(rentMsg);
1473
+ const key = `rent:${scheduleId}`;
1474
+ return new Promise((resolve2, reject) => {
1475
+ const timer = setTimeout(() => {
1476
+ this._pendingRents.delete(key);
1477
+ reject(new TimeoutError("Rent timed out after 90s"));
1478
+ }, 9e4);
1479
+ this._pendingRents.set(key, {
1480
+ scheduleId,
1481
+ eventId: null,
1482
+ opts,
1483
+ resolve: (match) => {
1484
+ const humanizer = this._resolveHumanizer(opts);
1485
+ const browser = new Browser(this, match, humanizer);
1486
+ this._activeBrowsers.set(browser.sessionId, browser);
1487
+ if (opts?.maskingMode) {
1488
+ browser.configure({ maskingMode: true }).catch(() => {
1489
+ });
1490
+ }
1491
+ if (opts?.fingerprint) {
1492
+ browser.configure({ fingerprint: opts.fingerprint }).catch(() => {
1493
+ });
1494
+ }
1495
+ resolve2(browser);
1496
+ },
1497
+ reject,
1498
+ timer
1499
+ });
1500
+ });
1501
+ }
1502
+ async resume(sessionId, opts) {
1503
+ this._wsSend({ type: "resume", session_id: sessionId });
1504
+ return new Promise((resolve2, reject) => {
1505
+ const timer = setTimeout(() => {
1506
+ this._pendingResumes.delete(sessionId);
1507
+ reject(new TimeoutError("Resume timed out after 10s"));
1508
+ }, 1e4);
1509
+ this._pendingResumes.set(sessionId, {
1510
+ sessionId,
1511
+ opts,
1512
+ resolve: (match) => {
1513
+ const humanizer = this._resolveHumanizer(opts);
1514
+ const browser = new Browser(this, match, humanizer);
1515
+ this._activeBrowsers.set(browser.sessionId, browser);
1516
+ resolve2(browser);
1517
+ },
1518
+ reject,
1519
+ timer
1520
+ });
1521
+ });
1522
+ }
1523
+ async close() {
1524
+ this._closed = true;
1525
+ const closePromises = [];
1526
+ for (const browser of this._activeBrowsers.values()) {
1527
+ closePromises.push(browser.close().catch(() => {
1528
+ }));
1529
+ }
1530
+ await Promise.allSettled(closePromises);
1531
+ for (const [key, pending] of this._pendingRents) {
1532
+ clearTimeout(pending.timer);
1533
+ pending.reject(new Error("Client closed"));
1534
+ }
1535
+ this._pendingRents.clear();
1536
+ for (const [key, pending] of this._pendingResumes) {
1537
+ clearTimeout(pending.timer);
1538
+ pending.reject(new Error("Client closed"));
1539
+ }
1540
+ this._pendingResumes.clear();
1541
+ this._stopHeartbeat();
1542
+ this._closeWs();
1543
+ }
1544
+ async disconnect() {
1545
+ this._closed = true;
1546
+ this._activeBrowsers.clear();
1547
+ this._pendingRents.clear();
1548
+ this._pendingResumes.clear();
1549
+ this._stopHeartbeat();
1550
+ this._closeWs();
1551
+ }
1552
+ // --- Private methods ---
1553
+ async _connect() {
1554
+ return new Promise((resolve2, reject) => {
1555
+ this._connectResolve = resolve2;
1556
+ this._connectReject = reject;
1557
+ this._openWs();
1558
+ });
1559
+ }
1560
+ _openWs() {
1561
+ const protocols = [`bearer.${this._apiKey}`];
1562
+ this._ws = new WebSocket(this._relayUrl, protocols);
1563
+ this._ws.on("open", () => {
1564
+ this._reconnectAttempt = 0;
1565
+ this._reconnecting = false;
1566
+ this._lastPongAt = Date.now();
1567
+ this._startHeartbeat();
1568
+ if (this._connectResolve) {
1569
+ this._connectResolve();
1570
+ this._connectResolve = null;
1571
+ this._connectReject = null;
1572
+ }
1573
+ });
1574
+ this._ws.on("message", (data) => {
1575
+ this._handleMessage(data);
1576
+ });
1577
+ this._ws.on("close", (code, reason) => {
1578
+ this._stopHeartbeat();
1579
+ const reasonStr = reason.toString();
1580
+ if (code === 4401 || code === 4403) {
1581
+ const err2 = new AuthError(reasonStr || `Auth failed (${code})`);
1582
+ if (this._connectReject) {
1583
+ this._connectReject(err2);
1584
+ this._connectResolve = null;
1585
+ this._connectReject = null;
1586
+ }
1587
+ return;
1588
+ }
1589
+ if (!this._closed && this._reconnect) {
1590
+ this._scheduleReconnect();
1591
+ }
1592
+ });
1593
+ this._ws.on("error", (err2) => {
1594
+ if (this._connectReject) {
1595
+ this._connectReject(new TransportError(err2.message));
1596
+ this._connectResolve = null;
1597
+ this._connectReject = null;
1598
+ }
1599
+ });
1600
+ }
1601
+ _closeWs() {
1602
+ if (this._ws) {
1603
+ try {
1604
+ this._ws.removeAllListeners();
1605
+ this._ws.close();
1606
+ } catch {
1607
+ }
1608
+ this._ws = null;
1609
+ }
1610
+ }
1611
+ _scheduleReconnect() {
1612
+ if (this._closed || this._reconnecting) return;
1613
+ if (this._reconnectAttempt >= MAX_RECONNECT_ATTEMPTS) {
1614
+ const err2 = new ConnectionLost("Max reconnection attempts exceeded");
1615
+ this._rejectAllPending(err2);
1616
+ return;
1617
+ }
1618
+ this._reconnecting = true;
1619
+ const backoffIdx = Math.min(this._reconnectAttempt, BACKOFF_SCHEDULE.length - 1);
1620
+ const delay = BACKOFF_SCHEDULE[backoffIdx] * 1e3;
1621
+ this._reconnectAttempt++;
1622
+ setTimeout(() => {
1623
+ if (this._closed) return;
1624
+ this._closeWs();
1625
+ this._connectResolve = () => {
1626
+ for (const browser of this._activeBrowsers.values()) {
1627
+ this._wsSend({ type: "resume", session_id: browser.sessionId });
1628
+ }
1629
+ };
1630
+ this._connectReject = () => {
1631
+ this._reconnecting = false;
1632
+ this._scheduleReconnect();
1633
+ };
1634
+ this._openWs();
1635
+ }, delay);
1636
+ }
1637
+ _startHeartbeat() {
1638
+ this._stopHeartbeat();
1639
+ this._pingTimer = setInterval(() => {
1640
+ try {
1641
+ this._wsSend({ type: "ping" });
1642
+ } catch {
1643
+ }
1644
+ }, PING_INTERVAL);
1645
+ this._pongTimer = setTimeout(() => {
1646
+ this._checkPongTimeout();
1647
+ }, PONG_TIMEOUT);
1648
+ }
1649
+ _stopHeartbeat() {
1650
+ if (this._pingTimer) {
1651
+ clearInterval(this._pingTimer);
1652
+ this._pingTimer = null;
1653
+ }
1654
+ if (this._pongTimer) {
1655
+ clearTimeout(this._pongTimer);
1656
+ this._pongTimer = null;
1657
+ }
1658
+ }
1659
+ _checkPongTimeout() {
1660
+ if (this._closed) return;
1661
+ const elapsed = Date.now() - this._lastPongAt;
1662
+ if (elapsed >= PONG_TIMEOUT) {
1663
+ this._closeWs();
1664
+ if (this._reconnect) {
1665
+ this._scheduleReconnect();
1666
+ }
1667
+ } else {
1668
+ this._pongTimer = setTimeout(() => {
1669
+ this._checkPongTimeout();
1670
+ }, PONG_TIMEOUT - elapsed);
1671
+ }
1672
+ }
1673
+ _handleMessage(data) {
1674
+ let msg;
1675
+ try {
1676
+ msg = JSON.parse(data.toString());
1677
+ } catch {
1678
+ return;
1679
+ }
1680
+ const type = String(msg.type ?? "");
1681
+ const sessionId = msg.session_id ? String(msg.session_id) : null;
1682
+ switch (type) {
1683
+ case "pong":
1684
+ this._lastPongAt = Date.now();
1685
+ break;
1686
+ case "rent_pending":
1687
+ this._onRentPending(msg);
1688
+ break;
1689
+ case "match":
1690
+ this._onMatch(msg);
1691
+ break;
1692
+ case "rent.error":
1693
+ this._onRentError(msg);
1694
+ break;
1695
+ case "resume_ok":
1696
+ this._onResumeOk(msg);
1697
+ break;
1698
+ case "resume_failed":
1699
+ this._onResumeFailed(msg);
1700
+ break;
1701
+ case "cdp_response":
1702
+ if (sessionId) {
1703
+ const browser = this._activeBrowsers.get(sessionId);
1704
+ browser?._onCdpResponse(msg);
1705
+ }
1706
+ break;
1707
+ case "cdp_event":
1708
+ if (sessionId) {
1709
+ const browser = this._activeBrowsers.get(sessionId);
1710
+ browser?._onCdpEvent(msg);
1711
+ }
1712
+ break;
1713
+ case "tab_opened":
1714
+ if (sessionId) {
1715
+ const browser = this._activeBrowsers.get(sessionId);
1716
+ browser?._onTabOpened(msg);
1717
+ }
1718
+ break;
1719
+ case "session.ended":
1720
+ if (sessionId) {
1721
+ const browser = this._activeBrowsers.get(sessionId);
1722
+ browser?._onSessionEnded(msg);
1723
+ }
1724
+ break;
1725
+ case "session.provider_disconnected":
1726
+ if (sessionId) {
1727
+ const browser = this._activeBrowsers.get(sessionId);
1728
+ browser?._onProviderDisconnected();
1729
+ }
1730
+ break;
1731
+ case "session.provider_reconnected":
1732
+ if (sessionId) {
1733
+ const browser = this._activeBrowsers.get(sessionId);
1734
+ browser?._onProviderReconnected();
1735
+ }
1736
+ break;
1737
+ case "user_events":
1738
+ if (sessionId) {
1739
+ const browser = this._activeBrowsers.get(sessionId);
1740
+ browser?._onUserEvents(msg);
1741
+ }
1742
+ break;
1743
+ case "chat.message":
1744
+ if (sessionId) {
1745
+ const browser = this._activeBrowsers.get(sessionId);
1746
+ browser?._onChatMessage(msg.payload ?? msg);
1747
+ }
1748
+ break;
1749
+ case "chat.read":
1750
+ if (sessionId) {
1751
+ const browser = this._activeBrowsers.get(sessionId);
1752
+ browser?._onChatRead(msg.payload ?? msg);
1753
+ }
1754
+ break;
1755
+ case "chat.send_ack":
1756
+ if (sessionId) {
1757
+ const browser = this._activeBrowsers.get(sessionId);
1758
+ browser?._onChatSendAck(msg);
1759
+ }
1760
+ break;
1761
+ case "chat.error":
1762
+ if (sessionId) {
1763
+ const browser = this._activeBrowsers.get(sessionId);
1764
+ browser?._onChatSendError(msg);
1765
+ }
1766
+ break;
1767
+ case "error":
1768
+ this._onError(msg);
1769
+ break;
1770
+ }
1771
+ }
1772
+ _onRentPending(msg) {
1773
+ const eventId = String(msg.event_id ?? "");
1774
+ for (const [key, pending] of this._pendingRents) {
1775
+ if (!pending.eventId) {
1776
+ pending.eventId = eventId;
1777
+ this._pendingRents.delete(key);
1778
+ this._pendingRents.set(`event:${eventId}`, pending);
1779
+ break;
1780
+ }
1781
+ }
1782
+ }
1783
+ _onMatch(msg) {
1784
+ const eventId = String(msg.event_id ?? "");
1785
+ const scheduleId = Number(msg.schedule_id ?? 0);
1786
+ const sessionId = String(msg.session_id ?? "");
1787
+ if (msg.requires_ack) {
1788
+ try {
1789
+ this._wsSend({ type: "match_ack", session_id: sessionId });
1790
+ } catch {
1791
+ }
1792
+ }
1793
+ let pending = this._pendingRents.get(`event:${eventId}`);
1794
+ if (!pending) {
1795
+ pending = this._pendingRents.get(`rent:${scheduleId}`);
1796
+ }
1797
+ if (pending) {
1798
+ clearTimeout(pending.timer);
1799
+ const key = pending.eventId ? `event:${pending.eventId}` : `rent:${pending.scheduleId}`;
1800
+ this._pendingRents.delete(key);
1801
+ const match = {
1802
+ session_id: sessionId,
1803
+ schedule_id: scheduleId,
1804
+ event_id: eventId || null,
1805
+ chat_topic_id: msg.chat_topic_id ? String(msg.chat_topic_id) : null,
1806
+ provider_user_id: msg.provider_user_id != null ? Number(msg.provider_user_id) : null,
1807
+ started_at: Date.now(),
1808
+ browser_info: msg.browser_info ?? {}
1809
+ };
1810
+ pending.resolve(match);
1811
+ }
1812
+ }
1813
+ _onRentError(msg) {
1814
+ const eventId = String(msg.event_id ?? "");
1815
+ const code = String(msg.code ?? "");
1816
+ const message = String(msg.message ?? "");
1817
+ let pending = this._pendingRents.get(`event:${eventId}`);
1818
+ if (!pending) {
1819
+ for (const [key, p] of this._pendingRents) {
1820
+ pending = p;
1821
+ this._pendingRents.delete(key);
1822
+ break;
1823
+ }
1824
+ } else {
1825
+ this._pendingRents.delete(`event:${eventId}`);
1826
+ }
1827
+ if (pending) {
1828
+ clearTimeout(pending.timer);
1829
+ if (code === "provider_offline") {
1830
+ pending.reject(new ProviderOffline(message));
1831
+ } else {
1832
+ pending.reject(new TransportError(message || `Rent error: ${code}`));
1833
+ }
1834
+ }
1835
+ }
1836
+ _onResumeOk(msg) {
1837
+ const sessionId = String(msg.session_id ?? "");
1838
+ const pending = this._pendingResumes.get(sessionId);
1839
+ if (!pending) return;
1840
+ clearTimeout(pending.timer);
1841
+ this._pendingResumes.delete(sessionId);
1842
+ const match = {
1843
+ session_id: sessionId,
1844
+ event_id: msg.event_id != null ? String(msg.event_id) : null,
1845
+ schedule_id: Number(msg.schedule_id ?? 0),
1846
+ chat_topic_id: msg.chat_topic_id ? String(msg.chat_topic_id) : null,
1847
+ provider_user_id: msg.provider_user_id != null ? Number(msg.provider_user_id) : null,
1848
+ started_at: Date.now(),
1849
+ browser_info: msg.browser_info ?? {}
1850
+ };
1851
+ pending.resolve(match);
1852
+ }
1853
+ _onResumeFailed(msg) {
1854
+ const sessionId = String(msg.session_id ?? "");
1855
+ const reason = String(msg.reason ?? "");
1856
+ const pending = this._pendingResumes.get(sessionId);
1857
+ if (!pending) return;
1858
+ clearTimeout(pending.timer);
1859
+ this._pendingResumes.delete(sessionId);
1860
+ switch (reason) {
1861
+ case "expired":
1862
+ pending.reject(new SessionExpired());
1863
+ break;
1864
+ case "not_owner":
1865
+ pending.reject(new NotOwner());
1866
+ break;
1867
+ case "not_found":
1868
+ pending.reject(new SessionNotFound());
1869
+ break;
1870
+ default:
1871
+ pending.reject(new SessionNotFound(reason));
1872
+ }
1873
+ }
1874
+ _onError(msg) {
1875
+ const code = Number(msg.code ?? 0);
1876
+ const reason = String(msg.reason ?? msg.message ?? "");
1877
+ const sessionId = msg.session_id ? String(msg.session_id) : null;
1878
+ if (sessionId) {
1879
+ const browser = this._activeBrowsers.get(sessionId);
1880
+ if (browser) {
1881
+ switch (code) {
1882
+ case -1011:
1883
+ browser._onSessionEnded({ reason: "heartbeat_timeout" });
1884
+ return;
1885
+ case -1012:
1886
+ browser._onError({ reason: "insufficient_funds" });
1887
+ return;
1888
+ case -1013:
1889
+ browser._onError({ reason: "rate_limit_exceeded" });
1890
+ return;
1891
+ case -1015:
1892
+ browser._onSessionEnded({ reason: "provider_declined" });
1893
+ return;
1894
+ case -1018:
1895
+ browser._onSessionEnded({ reason: "killed" });
1896
+ return;
1897
+ case -1050:
1898
+ browser._onError({ reason: `cdp_unrecoverable: ${reason}` });
1899
+ return;
1900
+ default:
1901
+ browser._onError(msg);
1902
+ return;
1903
+ }
1904
+ }
1905
+ }
1906
+ let err2;
1907
+ switch (code) {
1908
+ case -1012:
1909
+ err2 = new InsufficientFunds(reason);
1910
+ this._rejectAllPending(err2);
1911
+ break;
1912
+ case -1013:
1913
+ err2 = new RateLimitExceeded(0, reason);
1914
+ this._rejectAllPending(err2);
1915
+ break;
1916
+ case -1015:
1917
+ err2 = new ProviderOffline(reason);
1918
+ this._rejectFirstPendingRent(err2);
1919
+ break;
1920
+ default:
1921
+ err2 = new CekiBrowserError(reason || `relay error ${code}`);
1922
+ this._rejectFirstPendingRent(err2);
1923
+ break;
1924
+ }
1925
+ }
1926
+ _rejectFirstPendingRent(err2) {
1927
+ const eventId = this._pendingRents.keys().next().value;
1928
+ if (eventId != null) {
1929
+ const pending = this._pendingRents.get(eventId);
1930
+ clearTimeout(pending.timer);
1931
+ this._pendingRents.delete(eventId);
1932
+ pending.reject(err2);
1933
+ }
1934
+ }
1935
+ _rejectAllPending(err2) {
1936
+ for (const [key, pending] of this._pendingRents) {
1937
+ clearTimeout(pending.timer);
1938
+ pending.reject(err2);
1939
+ }
1940
+ this._pendingRents.clear();
1941
+ for (const [key, pending] of this._pendingResumes) {
1942
+ clearTimeout(pending.timer);
1943
+ pending.reject(err2);
1944
+ }
1945
+ this._pendingResumes.clear();
1946
+ }
1947
+ _resolveHumanizer(opts) {
1948
+ if (process.env.CEKI_HUMAN_DISABLE === "1") return null;
1949
+ const human = opts?.human;
1950
+ if (human === null || human === void 0) {
1951
+ const envPreset = process.env.CEKI_HUMAN_PROFILE;
1952
+ const envPath = process.env.CEKI_HUMAN_PROFILE_PATH;
1953
+ if (envPath) return new Humanizer(HumanProfile.load(envPath));
1954
+ if (envPreset) return new Humanizer(HumanProfile.loadPreset(envPreset));
1955
+ return new Humanizer(HumanProfile.loadPreset("natural"));
1956
+ }
1957
+ if (human === "natural" || human === "careful") {
1958
+ return new Humanizer(HumanProfile.loadPreset(human));
1959
+ }
1960
+ return null;
1961
+ }
1962
+ };
1963
+ async function connect(apiKey, opts) {
1964
+ return Client.create(apiKey, opts);
1965
+ }
1966
+
1967
+ // src/cli.ts
1968
+ function out(data) {
1969
+ process.stdout.write(JSON.stringify(data) + "\n");
1970
+ }
1971
+ function err(error, code = "error") {
1972
+ process.stderr.write(JSON.stringify({ error, code }) + "\n");
1973
+ }
1974
+ function getApiKey() {
1975
+ const key = process.env.CEKI_API_KEY;
1976
+ if (!key) {
1977
+ err("CEKI_API_KEY not set", "auth");
1978
+ process.exit(2);
1979
+ }
1980
+ return key;
1981
+ }
1982
+ function connectOptions() {
1983
+ return { reconnect: false };
1984
+ }
1985
+ async function resumeBrowser(apiKey, sessionId) {
1986
+ const client = await connect(apiKey, connectOptions());
1987
+ const browser = await client.resume(sessionId, { human: null });
1988
+ return [client, browser];
1989
+ }
1990
+ async function closeClient(client) {
1991
+ try {
1992
+ await client.disconnect();
1993
+ } catch {
1994
+ }
1995
+ }
1996
+ function parseBool(val) {
1997
+ return val === "true" || val === "1" || val === "yes";
1998
+ }
1999
+ async function cmdRent(args) {
2000
+ let scheduleId = null;
2001
+ let fingerprintFrom = null;
2002
+ let mode = "incognito";
2003
+ for (let i = 0; i < args.length; i++) {
2004
+ if (args[i] === "--schedule" && args[i + 1]) scheduleId = parseInt(args[++i], 10);
2005
+ if (args[i] === "--fingerprint-from" && args[i + 1]) fingerprintFrom = args[++i];
2006
+ if (args[i] === "--mode" && args[i + 1]) {
2007
+ const v = args[++i];
2008
+ if (v !== "incognito" && v !== "main") {
2009
+ err("invalid mode, must be incognito or main", "args");
2010
+ process.exit(1);
2011
+ }
2012
+ mode = v;
2013
+ }
2014
+ }
2015
+ if (scheduleId == null) {
2016
+ err("--schedule is required", "args");
2017
+ process.exit(1);
2018
+ }
2019
+ const apiKey = getApiKey();
2020
+ let fpData = true;
2021
+ if (fingerprintFrom) {
2022
+ const profile = JSON.parse(fs4.readFileSync(fingerprintFrom, "utf-8"));
2023
+ fpData = profile.fingerprint || true;
2024
+ }
2025
+ const client = await connect(apiKey, connectOptions());
2026
+ try {
2027
+ const browser = await client.rent(scheduleId, { human: null, fingerprint: fpData, mode });
2028
+ saveSession(browser.sessionId, {
2029
+ session_id: browser.sessionId,
2030
+ chat_topic_id: browser.chatTopicId,
2031
+ schedule_id: browser.scheduleId,
2032
+ last_seen_ts: null
2033
+ });
2034
+ out({
2035
+ session_id: browser.sessionId,
2036
+ chat_topic_id: browser.chatTopicId,
2037
+ schedule_id: browser.scheduleId
2038
+ });
2039
+ } finally {
2040
+ await closeClient(client);
2041
+ }
2042
+ }
2043
+ async function cmdSearch(args) {
2044
+ let limit = 20;
2045
+ const filters = {};
2046
+ for (let i = 0; i < args.length; i++) {
2047
+ if (args[i] === "--limit" && args[i + 1]) limit = parseInt(args[++i], 10);
2048
+ if (args[i] === "--filter" && args[i + 1]) {
2049
+ const [k, ...v] = args[++i].split("=");
2050
+ filters[k] = v.join("=");
2051
+ }
2052
+ }
2053
+ const apiKey = getApiKey();
2054
+ const client = await connect(apiKey, connectOptions());
2055
+ try {
2056
+ const results = await client.search(filters, limit);
2057
+ out(results);
2058
+ } finally {
2059
+ await closeClient(client);
2060
+ }
2061
+ }
2062
+ async function cmdSessions(args) {
2063
+ let showAll = false;
2064
+ let limit = 50;
2065
+ let jsonOutput = false;
2066
+ for (let i = 0; i < args.length; i++) {
2067
+ if (args[i] === "--all") showAll = true;
2068
+ else if (args[i] === "--json") jsonOutput = true;
2069
+ else if (args[i] === "--limit" && args[i + 1]) limit = Number(args[++i]);
2070
+ }
2071
+ const apiKey = getApiKey();
2072
+ const client = await connect(apiKey, connectOptions());
2073
+ try {
2074
+ const results = await client.listSessions({ active: !showAll, limit });
2075
+ if (jsonOutput) {
2076
+ out(results);
2077
+ } else {
2078
+ if (!results.length) {
2079
+ process.stdout.write("No sessions found.\n");
2080
+ return;
2081
+ }
2082
+ const header = "SID".padEnd(8) + "SCHEDULE".padEnd(10) + "STARTED".padEnd(22) + "DURATION".padEnd(10) + "EARNED".padEnd(9) + "STATUS".padEnd(10) + "RENTER".padEnd(16) + "PROVIDER";
2083
+ process.stdout.write(header + "\n");
2084
+ for (const s of results) {
2085
+ const started = s.started_at ?? "\u2014";
2086
+ const mins = Math.floor(s.duration / 60);
2087
+ const secs = s.duration % 60;
2088
+ const dur = `${mins}:${String(secs).padStart(2, "0")}`;
2089
+ const earned = `$${s.earned.toFixed(2)}`;
2090
+ const renter = s.renter?.name ?? "\u2014";
2091
+ const provider = s.provider?.name ?? "\u2014";
2092
+ const line = String(s.id).padEnd(8) + String(s.schedule_id).padEnd(10) + started.padEnd(22) + dur.padEnd(10) + earned.padEnd(9) + s.status.padEnd(10) + renter.padEnd(16) + provider;
2093
+ process.stdout.write(line + "\n");
2094
+ }
2095
+ }
2096
+ } finally {
2097
+ await closeClient(client);
2098
+ }
2099
+ }
2100
+ async function cmdMyBrowsers() {
2101
+ const apiKey = getApiKey();
2102
+ const client = await connect(apiKey, connectOptions());
2103
+ try {
2104
+ const results = await client.myBrowsers();
2105
+ out(results);
2106
+ } finally {
2107
+ await closeClient(client);
2108
+ }
2109
+ }
2110
+ async function cmdSnapshot(sid, args) {
2111
+ let outputPath = null;
2112
+ for (let i = 0; i < args.length; i++) {
2113
+ if ((args[i] === "-o" || args[i] === "--output") && args[i + 1]) outputPath = args[++i];
2114
+ }
2115
+ if (!outputPath) {
2116
+ err("-o/--output is required", "args");
2117
+ process.exit(1);
2118
+ }
2119
+ const apiKey = getApiKey();
2120
+ const [client, browser] = await resumeBrowser(apiKey, sid);
2121
+ try {
2122
+ const lastSeen = getLastSeenTs(sid);
2123
+ browser._lastSeenTs = lastSeen;
2124
+ const snap = await browser.snapshot();
2125
+ const pngBuf = Buffer.from(snap.screenshot, "base64");
2126
+ fs4.writeFileSync(outputPath, pngBuf);
2127
+ if (browser._lastSeenTs) {
2128
+ updateLastSeenTs(sid, browser._lastSeenTs);
2129
+ }
2130
+ const chatList = snap.chat.map((m) => ({
2131
+ from: m.sender_id,
2132
+ text: m.text,
2133
+ ts: m.created_at
2134
+ }));
2135
+ out({ screenshot: outputPath, chat: chatList, ts: snap.ts.toISOString() });
2136
+ } finally {
2137
+ await closeClient(client);
2138
+ }
2139
+ }
2140
+ async function cmdNavigate(sid, args) {
2141
+ const url = args[0];
2142
+ if (!url) {
2143
+ err("URL is required", "args");
2144
+ process.exit(1);
2145
+ }
2146
+ const apiKey = getApiKey();
2147
+ const [client, browser] = await resumeBrowser(apiKey, sid);
2148
+ try {
2149
+ await browser.navigate(url);
2150
+ out({ ok: true });
2151
+ } finally {
2152
+ await closeClient(client);
2153
+ }
2154
+ }
2155
+ async function cmdClick(sid, args) {
2156
+ const x = parseInt(args[0], 10);
2157
+ const y = parseInt(args[1], 10);
2158
+ if (isNaN(x) || isNaN(y)) {
2159
+ err("x and y coordinates are required", "args");
2160
+ process.exit(1);
2161
+ }
2162
+ const apiKey = getApiKey();
2163
+ const [client, browser] = await resumeBrowser(apiKey, sid);
2164
+ try {
2165
+ await browser.click(x, y);
2166
+ out({ ok: true, pointer: [x, y] });
2167
+ } finally {
2168
+ await closeClient(client);
2169
+ }
2170
+ }
2171
+ async function cmdType(sid, args) {
2172
+ let text = "";
2173
+ let natural = false;
2174
+ for (let i = 0; i < args.length; i++) {
2175
+ if (args[i] === "--natural") natural = true;
2176
+ else if (!text) text = args[i];
2177
+ }
2178
+ if (!text) {
2179
+ err("text is required", "args");
2180
+ process.exit(1);
2181
+ }
2182
+ const apiKey = getApiKey();
2183
+ const human = natural ? "natural" : null;
2184
+ const client = await connect(apiKey, connectOptions());
2185
+ try {
2186
+ const browser = await client.resume(sid, { human });
2187
+ if (!natural) {
2188
+ browser._humanizer = null;
2189
+ }
2190
+ await browser.type(text);
2191
+ out({ ok: true });
2192
+ } finally {
2193
+ await closeClient(client);
2194
+ }
2195
+ }
2196
+ async function cmdScroll(sid, args) {
2197
+ const x = parseInt(args[0], 10);
2198
+ const y = parseInt(args[1], 10);
2199
+ const dy = parseInt(args[2], 10);
2200
+ if (isNaN(x) || isNaN(y) || isNaN(dy)) {
2201
+ err("x, y, dy are required", "args");
2202
+ process.exit(1);
2203
+ }
2204
+ const apiKey = getApiKey();
2205
+ const [client, browser] = await resumeBrowser(apiKey, sid);
2206
+ try {
2207
+ await browser.scroll({ x, y, deltaY: dy });
2208
+ out({ ok: true });
2209
+ } finally {
2210
+ await closeClient(client);
2211
+ }
2212
+ }
2213
+ async function cmdChat(sid, action, args) {
2214
+ const apiKey = getApiKey();
2215
+ const [client, browser] = await resumeBrowser(apiKey, sid);
2216
+ try {
2217
+ switch (action) {
2218
+ case "send": {
2219
+ const text = args[0];
2220
+ if (!text) {
2221
+ err("text is required", "args");
2222
+ process.exit(1);
2223
+ }
2224
+ const result = await browser.chat.send(text);
2225
+ out({ ok: true, message_id: result.messageId });
2226
+ break;
2227
+ }
2228
+ case "send-image": {
2229
+ let imagePath = null;
2230
+ let text;
2231
+ for (let i = 0; i < args.length; i++) {
2232
+ if (args[i] === "--image" && args[i + 1]) imagePath = args[++i];
2233
+ if (args[i] === "--text" && args[i + 1]) text = args[++i];
2234
+ }
2235
+ if (!imagePath) {
2236
+ err("--image is required", "args");
2237
+ process.exit(1);
2238
+ }
2239
+ if (text) {
2240
+ await browser.chat.send(text);
2241
+ }
2242
+ const result = await browser.chat.sendImage(imagePath);
2243
+ out({ ok: true, message_id: result.messageId });
2244
+ break;
2245
+ }
2246
+ case "next": {
2247
+ let timeout = 60;
2248
+ for (let i = 0; i < args.length; i++) {
2249
+ if (args[i] === "--timeout" && args[i + 1]) timeout = parseFloat(args[++i]);
2250
+ }
2251
+ const lastSeen = getLastSeenTs(sid);
2252
+ const msgs = await browser.chat.history({ since: lastSeen ?? void 0 });
2253
+ if (msgs.length > 0) {
2254
+ const m = msgs[0];
2255
+ updateLastSeenTs(sid, m.created_at);
2256
+ out({ from: m.sender_id, text: m.text, ts: m.created_at });
2257
+ } else {
2258
+ let resolved = false;
2259
+ const waitPromise = new Promise((resolve2) => {
2260
+ const timer = setTimeout(() => {
2261
+ if (!resolved) {
2262
+ resolved = true;
2263
+ resolve2(null);
2264
+ }
2265
+ }, timeout * 1e3);
2266
+ browser.chat.onMessage((msg2) => {
2267
+ if (!resolved) {
2268
+ resolved = true;
2269
+ clearTimeout(timer);
2270
+ resolve2(msg2);
2271
+ }
2272
+ });
2273
+ });
2274
+ const msg = await waitPromise;
2275
+ if (msg) {
2276
+ updateLastSeenTs(sid, msg.created_at);
2277
+ out({ from: msg.sender_id, text: msg.text, ts: msg.created_at });
2278
+ } else {
2279
+ out(null);
2280
+ }
2281
+ }
2282
+ break;
2283
+ }
2284
+ case "history": {
2285
+ let since;
2286
+ let limit = 50;
2287
+ for (let i = 0; i < args.length; i++) {
2288
+ if (args[i] === "--since" && args[i + 1]) {
2289
+ const val = args[++i];
2290
+ const asNum = Number(val);
2291
+ if (!isNaN(asNum) && val.match(/^\d+(\.\d+)?$/)) {
2292
+ since = new Date(asNum * 1e3).toISOString();
2293
+ } else {
2294
+ since = val;
2295
+ }
2296
+ }
2297
+ if (args[i] === "--limit" && args[i + 1]) limit = parseInt(args[++i], 10);
2298
+ }
2299
+ const msgs = await browser.chat.history({ since, limit });
2300
+ out(msgs.map((m) => ({ from: m.sender_id, text: m.text, ts: m.created_at })));
2301
+ break;
2302
+ }
2303
+ default:
2304
+ err(`Unknown chat action: ${action}`, "args");
2305
+ process.exit(1);
2306
+ }
2307
+ } finally {
2308
+ await closeClient(client);
2309
+ }
2310
+ }
2311
+ async function cmdStop(sid) {
2312
+ const apiKey = getApiKey();
2313
+ const [client, browser] = await resumeBrowser(apiKey, sid);
2314
+ try {
2315
+ await browser.close();
2316
+ deleteSession(sid);
2317
+ out({ ok: true });
2318
+ } finally {
2319
+ await closeClient(client);
2320
+ }
2321
+ }
2322
+ async function cmdProfile(sid, action, args) {
2323
+ const apiKey = getApiKey();
2324
+ const [client, browser] = await resumeBrowser(apiKey, sid);
2325
+ try {
2326
+ switch (action) {
2327
+ case "export": {
2328
+ let outputPath = null;
2329
+ let domains;
2330
+ let noSessionStorage = false;
2331
+ for (let i = 0; i < args.length; i++) {
2332
+ if ((args[i] === "-o" || args[i] === "--output") && args[i + 1]) outputPath = args[++i];
2333
+ if (args[i] === "--domains" && args[i + 1]) domains = args[++i].split(",").map((d) => d.trim());
2334
+ if (args[i] === "--no-session-storage") noSessionStorage = true;
2335
+ }
2336
+ if (!outputPath) {
2337
+ err("-o/--output is required", "args");
2338
+ process.exit(1);
2339
+ }
2340
+ const profile = await browser.profile.export({
2341
+ domains,
2342
+ includeSessionStorage: !noSessionStorage
2343
+ });
2344
+ fs4.writeFileSync(outputPath, JSON.stringify(profile, null, 2), "utf-8");
2345
+ out({ ok: true, path: outputPath });
2346
+ break;
2347
+ }
2348
+ case "import": {
2349
+ let inputPath = null;
2350
+ for (let i = 0; i < args.length; i++) {
2351
+ if ((args[i] === "-i" || args[i] === "--input") && args[i + 1]) inputPath = args[++i];
2352
+ }
2353
+ if (!inputPath) {
2354
+ err("-i/--input is required", "args");
2355
+ process.exit(1);
2356
+ }
2357
+ const profileData = JSON.parse(fs4.readFileSync(inputPath, "utf-8"));
2358
+ await browser.profile.import(profileData);
2359
+ out({ ok: true });
2360
+ break;
2361
+ }
2362
+ default:
2363
+ err(`Unknown profile action: ${action}`, "args");
2364
+ process.exit(1);
2365
+ }
2366
+ } finally {
2367
+ await closeClient(client);
2368
+ }
2369
+ }
2370
+ async function cmdWait(sid) {
2371
+ const apiKey = getApiKey();
2372
+ const [client, browser] = await resumeBrowser(apiKey, sid);
2373
+ try {
2374
+ const reason = await browser.waitUntilEnded();
2375
+ out({ ended: true, reason });
2376
+ } finally {
2377
+ await closeClient(client);
2378
+ }
2379
+ }
2380
+ async function cmdScreenshot(sid, args) {
2381
+ let outputPath = null;
2382
+ let fullPage = false;
2383
+ for (let i = 0; i < args.length; i++) {
2384
+ if ((args[i] === "-o" || args[i] === "--output") && args[i + 1]) outputPath = args[++i];
2385
+ if (args[i] === "--full") fullPage = true;
2386
+ }
2387
+ if (!outputPath) {
2388
+ err("-o/--output is required", "args");
2389
+ process.exit(1);
2390
+ }
2391
+ const apiKey = getApiKey();
2392
+ const [client, browser] = await resumeBrowser(apiKey, sid);
2393
+ try {
2394
+ const data = await browser.screenshot({ format: "png", fullPage });
2395
+ fs4.writeFileSync(outputPath, data);
2396
+ out({ ok: true, path: outputPath });
2397
+ } finally {
2398
+ await closeClient(client);
2399
+ }
2400
+ }
2401
+ async function cmdSwitchTab(sid) {
2402
+ const apiKey = getApiKey();
2403
+ const [client, browser] = await resumeBrowser(apiKey, sid);
2404
+ try {
2405
+ await browser.switchTab();
2406
+ out({ ok: true });
2407
+ } finally {
2408
+ await closeClient(client);
2409
+ }
2410
+ }
2411
+ async function cmdConfigure(sid, args) {
2412
+ const opts = {};
2413
+ for (let i = 0; i < args.length; i++) {
2414
+ if (args[i] === "--masking-mode" && args[i + 1]) opts.maskingMode = parseBool(args[++i]);
2415
+ if (args[i] === "--fingerprint" && args[i + 1]) opts.fingerprint = parseBool(args[++i]);
2416
+ }
2417
+ const apiKey = getApiKey();
2418
+ const [client, browser] = await resumeBrowser(apiKey, sid);
2419
+ try {
2420
+ await browser.configure(opts);
2421
+ out({ ok: true });
2422
+ } finally {
2423
+ await closeClient(client);
2424
+ }
2425
+ }
2426
+ async function cmdCdp(sid, args) {
2427
+ let method = null;
2428
+ let params = {};
2429
+ for (let i = 0; i < args.length; i++) {
2430
+ if (args[i] === "--method" && args[i + 1]) method = args[++i];
2431
+ if (args[i] === "--params" && args[i + 1]) params = JSON.parse(args[++i]);
2432
+ }
2433
+ if (!method) {
2434
+ err("--method is required", "args");
2435
+ process.exit(1);
2436
+ }
2437
+ const apiKey = getApiKey();
2438
+ const [client, browser] = await resumeBrowser(apiKey, sid);
2439
+ try {
2440
+ const result = await browser.send({ method, params });
2441
+ out(result);
2442
+ } finally {
2443
+ await closeClient(client);
2444
+ }
2445
+ }
2446
+ async function cmdRequestCaptcha(sid, args) {
2447
+ let acceptance = 60;
2448
+ let completion = 120;
2449
+ let manual = false;
2450
+ for (let i = 0; i < args.length; i++) {
2451
+ if (args[i] === "--acceptance" && args[i + 1]) acceptance = parseFloat(args[++i]);
2452
+ if (args[i] === "--completion" && args[i + 1]) completion = parseFloat(args[++i]);
2453
+ if (args[i] === "--manual") manual = true;
2454
+ }
2455
+ const apiKey = getApiKey();
2456
+ const [client, browser] = await resumeBrowser(apiKey, sid);
2457
+ try {
2458
+ const result = await browser.requestCaptcha({
2459
+ acceptanceTimeout: acceptance,
2460
+ completionTimeout: completion,
2461
+ autoAccept: !manual
2462
+ });
2463
+ out({
2464
+ solved: result.solved,
2465
+ proof_message_id: result.proofMessageId,
2466
+ cancel_reason: result.cancelReason,
2467
+ child_event_id: result.childEventId,
2468
+ correction_id: result.correctionId
2469
+ });
2470
+ if (!result.solved) process.exit(1);
2471
+ } catch (e) {
2472
+ if (e instanceof CaptchaTimeoutError) {
2473
+ out({ solved: false, cancel_reason: `timeout:${e.phase}`, child_event_id: null, correction_id: null });
2474
+ process.exit(1);
2475
+ }
2476
+ throw e;
2477
+ } finally {
2478
+ await closeClient(client);
2479
+ }
2480
+ }
2481
+ async function cmdUpload(sid, args) {
2482
+ let selector = null;
2483
+ let filePath = null;
2484
+ let filename;
2485
+ for (let i = 0; i < args.length; i++) {
2486
+ if (args[i] === "--selector" && args[i + 1]) selector = args[++i];
2487
+ if (args[i] === "--file" && args[i + 1]) filePath = args[++i];
2488
+ if (args[i] === "--filename" && args[i + 1]) filename = args[++i];
2489
+ }
2490
+ if (!selector || !filePath) {
2491
+ err("--selector and --file are required", "args");
2492
+ process.exit(1);
2493
+ }
2494
+ if (!fs4.existsSync(filePath)) {
2495
+ err(`File not found: ${filePath}`, "args");
2496
+ process.exit(1);
2497
+ }
2498
+ const apiKey = getApiKey();
2499
+ const [client, browser] = await resumeBrowser(apiKey, sid);
2500
+ try {
2501
+ const result = await browser.upload(selector, filePath, filename);
2502
+ out(result);
2503
+ } finally {
2504
+ await closeClient(client);
2505
+ }
2506
+ }
2507
+ function printHelp() {
2508
+ console.log(`ceki-browser \u2014 CLI for browser.ceki.me rental
2509
+
2510
+ Usage: ceki-browser <command> [options]
2511
+
2512
+ Commands:
2513
+ rent --schedule N [--fingerprint-from PATH]
2514
+ my-browsers
2515
+ search [--limit N] [--filter k=v]...
2516
+ snapshot <sid> -o PATH
2517
+ screenshot <sid> -o PATH [--full]
2518
+ navigate <sid> <url>
2519
+ click <sid> <x> <y>
2520
+ type <sid> "<text>" [--natural]
2521
+ scroll <sid> <x> <y> <dy>
2522
+ switch-tab <sid>
2523
+ configure <sid> [--masking-mode true|false] [--fingerprint true|false]
2524
+ cdp <sid> --method <M> [--params JSON]
2525
+ upload <sid> --selector CSS --file PATH [--filename NAME]
2526
+ request-captcha <sid> [--acceptance N] [--completion M] [--manual]
2527
+ wait <sid>
2528
+ chat <sid> send "<text>"
2529
+ chat <sid> send-image --image PATH [--text "..."]
2530
+ chat <sid> next [--timeout N]
2531
+ chat <sid> history [--since TS] [--limit N]
2532
+ profile export <sid> -o file [--domains a,b,c] [--no-session-storage]
2533
+ profile import <sid> -i file
2534
+ stop <sid>
2535
+
2536
+ Environment:
2537
+ CEKI_API_KEY (required)
2538
+ CEKI_RELAY_URL (default: wss://browser.ceki.me/ws/agent)
2539
+ CEKI_API_URL (default: https://api.ceki.me)
2540
+ CEKI_CHAT_URL (default: https://chat.ceki.me/api/chat)
2541
+ CEKI_BASIC_AUTH_USER / CEKI_BASIC_AUTH_PASS (optional)
2542
+
2543
+ Exit codes: 0=success, 1=error, 2=auth, 3=session_not_found, 4=timeout, 5=network`);
2544
+ }
2545
+ async function main() {
2546
+ const argv = process.argv.slice(2);
2547
+ if (argv.length === 0 || argv[0] === "--help" || argv[0] === "-h") {
2548
+ printHelp();
2549
+ process.exit(0);
2550
+ }
2551
+ if (argv[0] === "--version" || argv[0] === "-v") {
2552
+ const pkgPath = path3.resolve(path3.dirname(new URL(import.meta.url).pathname), "..", "package.json");
2553
+ try {
2554
+ const pkg = JSON.parse(fs4.readFileSync(pkgPath, "utf-8"));
2555
+ console.log(pkg.version);
2556
+ } catch {
2557
+ console.log("unknown");
2558
+ }
2559
+ process.exit(0);
2560
+ }
2561
+ const command = argv[0];
2562
+ const rest = argv.slice(1);
2563
+ switch (command) {
2564
+ case "rent":
2565
+ await cmdRent(rest);
2566
+ break;
2567
+ case "search":
2568
+ await cmdSearch(rest);
2569
+ break;
2570
+ case "sessions":
2571
+ await cmdSessions(rest);
2572
+ break;
2573
+ case "my-browsers":
2574
+ await cmdMyBrowsers();
2575
+ break;
2576
+ case "snapshot":
2577
+ await cmdSnapshot(rest[0], rest.slice(1));
2578
+ break;
2579
+ case "navigate":
2580
+ await cmdNavigate(rest[0], rest.slice(1));
2581
+ break;
2582
+ case "click":
2583
+ await cmdClick(rest[0], rest.slice(1));
2584
+ break;
2585
+ case "type":
2586
+ await cmdType(rest[0], rest.slice(1));
2587
+ break;
2588
+ case "scroll":
2589
+ await cmdScroll(rest[0], rest.slice(1));
2590
+ break;
2591
+ case "switch-tab":
2592
+ await cmdSwitchTab(rest[0]);
2593
+ break;
2594
+ case "configure":
2595
+ await cmdConfigure(rest[0], rest.slice(1));
2596
+ break;
2597
+ case "cdp":
2598
+ await cmdCdp(rest[0], rest.slice(1));
2599
+ break;
2600
+ case "upload":
2601
+ await cmdUpload(rest[0], rest.slice(1));
2602
+ break;
2603
+ case "request-captcha":
2604
+ await cmdRequestCaptcha(rest[0], rest.slice(1));
2605
+ break;
2606
+ case "wait":
2607
+ await cmdWait(rest[0]);
2608
+ break;
2609
+ case "stop":
2610
+ await cmdStop(rest[0]);
2611
+ break;
2612
+ case "screenshot":
2613
+ await cmdScreenshot(rest[0], rest.slice(1));
2614
+ break;
2615
+ case "chat":
2616
+ if (rest.length < 2) {
2617
+ err("Usage: ceki-browser chat <sid> <action> [args]", "args");
2618
+ process.exit(1);
2619
+ }
2620
+ await cmdChat(rest[0], rest[1], rest.slice(2));
2621
+ break;
2622
+ case "profile":
2623
+ if (rest.length < 2) {
2624
+ err("Usage: ceki-browser profile export|import <sid> [args]", "args");
2625
+ process.exit(1);
2626
+ }
2627
+ await cmdProfile(rest[1], rest[0], rest.slice(2));
2628
+ break;
2629
+ default:
2630
+ err(`Unknown command: ${command}`, "args");
2631
+ process.exit(1);
2632
+ }
2633
+ }
2634
+ main().catch((e) => {
2635
+ if (e instanceof SessionExpired || e instanceof SessionNotFound) {
2636
+ err(String(e), "session_not_found");
2637
+ process.exit(3);
2638
+ }
2639
+ if (e instanceof NotOwner) {
2640
+ err(String(e), "not_owner");
2641
+ process.exit(3);
2642
+ }
2643
+ if (e instanceof TimeoutError) {
2644
+ err(String(e), "timeout");
2645
+ process.exit(4);
2646
+ }
2647
+ if (e instanceof ConnectionLost || e instanceof AuthError || e instanceof TransportError) {
2648
+ err(String(e), "network");
2649
+ process.exit(5);
2650
+ }
2651
+ if (e instanceof CekiBrowserError) {
2652
+ err(String(e), "ceki_error");
2653
+ process.exit(1);
2654
+ }
2655
+ err(e instanceof Error ? e.message : String(e), "error");
2656
+ process.exit(1);
2657
+ });
2658
+ //# sourceMappingURL=cli.js.map