10x-chat 0.8.3 → 0.9.1
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/browser/daemon.d.ts +22 -29
- package/dist/browser/daemon.d.ts.map +1 -1
- package/dist/browser/daemon.js +500 -61
- package/dist/browser/daemon.js.map +1 -1
- package/dist/browser/manager.d.ts.map +1 -1
- package/dist/browser/manager.js +8 -53
- package/dist/browser/manager.js.map +1 -1
- package/dist/browser/page-utils.d.ts +4 -0
- package/dist/browser/page-utils.d.ts.map +1 -0
- package/dist/browser/page-utils.js +37 -0
- package/dist/browser/page-utils.js.map +1 -0
- package/dist/browser/server.d.ts +2 -0
- package/dist/browser/server.d.ts.map +1 -0
- package/dist/browser/server.js +510 -0
- package/dist/browser/server.js.map +1 -0
- package/dist/browser/tabs.d.ts +2 -16
- package/dist/browser/tabs.d.ts.map +1 -1
- package/dist/browser/tabs.js +6 -80
- package/dist/browser/tabs.js.map +1 -1
- package/dist/core/research-orchestrator.d.ts.map +1 -1
- package/dist/core/research-orchestrator.js +2 -3
- package/dist/core/research-orchestrator.js.map +1 -1
- package/dist/notebooklm/rpc/decoder.d.ts +1 -1
- package/dist/notebooklm/rpc/decoder.d.ts.map +1 -1
- package/dist/notebooklm/rpc/decoder.js +1 -1
- package/dist/notebooklm/rpc/decoder.js.map +1 -1
- package/dist/notebooklm/types.d.ts +1 -2
- package/dist/notebooklm/types.d.ts.map +1 -1
- package/dist/notebooklm/types.js +1 -2
- package/dist/notebooklm/types.js.map +1 -1
- package/dist/providers/claude.d.ts +1 -0
- package/dist/providers/claude.d.ts.map +1 -1
- package/dist/providers/claude.js +60 -1
- package/dist/providers/claude.js.map +1 -1
- package/dist/providers/grok.d.ts.map +1 -1
- package/dist/providers/grok.js +2 -3
- package/dist/providers/grok.js.map +1 -1
- package/dist/providers/perplexity.d.ts.map +1 -1
- package/dist/providers/perplexity.js +2 -5
- package/dist/providers/perplexity.js.map +1 -1
- package/package.json +1 -1
- package/skills/10x-chat/SKILL.md +98 -55
package/dist/browser/daemon.d.ts
CHANGED
|
@@ -1,39 +1,32 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
* new one each time.
|
|
7
|
-
*
|
|
8
|
-
* Usage:
|
|
9
|
-
* const browser = await getOrLaunchBrowserDaemon(headless);
|
|
10
|
-
* // ... use browser ...
|
|
11
|
-
* // On close: call stopDaemon() only when tab count hits zero.
|
|
12
|
-
*/
|
|
13
|
-
import { type Browser } from 'playwright';
|
|
1
|
+
import type { Browser, BrowserContext, Page } from 'playwright';
|
|
2
|
+
interface WaitForUrlPredicate {
|
|
3
|
+
mode: 'changes' | 'startsWith';
|
|
4
|
+
value: string;
|
|
5
|
+
}
|
|
14
6
|
export interface DaemonState {
|
|
15
|
-
/** PID of the Chromium server process. */
|
|
16
7
|
pid: number;
|
|
17
|
-
|
|
18
|
-
|
|
8
|
+
port: number;
|
|
9
|
+
token: string;
|
|
19
10
|
headless: boolean;
|
|
20
11
|
createdAt: string;
|
|
21
12
|
}
|
|
13
|
+
interface SharedBrowserLaunchOptions {
|
|
14
|
+
headless?: boolean;
|
|
15
|
+
url?: string;
|
|
16
|
+
}
|
|
17
|
+
interface BrowserSessionProxy {
|
|
18
|
+
context: BrowserContext;
|
|
19
|
+
page: Page;
|
|
20
|
+
close: () => Promise<void>;
|
|
21
|
+
}
|
|
22
22
|
export declare function getDaemonStatePath(): string;
|
|
23
|
-
export declare function readDaemonState(): Promise<DaemonState | null>;
|
|
24
23
|
export declare function clearDaemonState(): Promise<void>;
|
|
25
|
-
|
|
26
|
-
* Stop the daemon browser by killing its server process.
|
|
27
|
-
* Safe to call even if daemon is not running.
|
|
28
|
-
*/
|
|
24
|
+
export declare function readDaemonState(): Promise<DaemonState | null>;
|
|
29
25
|
export declare function stopDaemon(): Promise<void>;
|
|
30
|
-
/**
|
|
31
|
-
* Get an existing browser daemon (reconnect) or launch a new one.
|
|
32
|
-
* Returns a Playwright Browser connected to the shared server process.
|
|
33
|
-
*
|
|
34
|
-
* The returned Browser is a client connection — calling browser.close() on it
|
|
35
|
-
* disconnects this client but does NOT stop the server. Call stopDaemon()
|
|
36
|
-
* when you actually want to shut the server down (i.e. last tab closed).
|
|
37
|
-
*/
|
|
38
26
|
export declare function getOrLaunchBrowserDaemon(headless?: boolean): Promise<Browser>;
|
|
27
|
+
export declare function launchSharedBrowserSession(opts: SharedBrowserLaunchOptions): Promise<BrowserSessionProxy>;
|
|
28
|
+
export declare function registerDaemonTab(): Promise<string>;
|
|
29
|
+
export declare function unregisterDaemonTab(tabKey: string): Promise<number>;
|
|
30
|
+
export declare function readLiveTabCount(): Promise<number>;
|
|
31
|
+
export type { WaitForUrlPredicate };
|
|
39
32
|
//# sourceMappingURL=daemon.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"daemon.d.ts","sourceRoot":"","sources":["../../src/browser/daemon.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"daemon.d.ts","sourceRoot":"","sources":["../../src/browser/daemon.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,OAAO,EAAE,cAAc,EAAiC,IAAI,EAAE,MAAM,YAAY,CAAC;AAiD/F,UAAU,mBAAmB;IAC3B,IAAI,EAAE,SAAS,GAAG,YAAY,CAAC;IAC/B,KAAK,EAAE,MAAM,CAAC;CACf;AASD,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,OAAO,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,UAAU,0BAA0B;IAClC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,UAAU,mBAAmB;IAC3B,OAAO,EAAE,cAAc,CAAC;IACxB,IAAI,EAAE,IAAI,CAAC;IACX,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5B;AA2XD,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C;AAED,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC,CAItD;AAED,wBAAsB,eAAe,IAAI,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CA2BnE;AAmHD,wBAAsB,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CAsBhD;AAED,wBAAsB,wBAAwB,CAAC,QAAQ,UAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAGhF;AAED,wBAAsB,0BAA0B,CAC9C,IAAI,EAAE,0BAA0B,GAC/B,OAAO,CAAC,mBAAmB,CAAC,CAuC9B;AAED,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,MAAM,CAAC,CASzD;AAED,wBAAsB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAgBzE;AAED,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,MAAM,CAAC,CAUxD;AAED,YAAY,EAAE,mBAAmB,EAAE,CAAC"}
|
package/dist/browser/daemon.js
CHANGED
|
@@ -1,101 +1,540 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
* Persists a browser server's PID + WS endpoint to disk so that separate
|
|
5
|
-
* CLI invocations can connect to the same browser instead of launching a
|
|
6
|
-
* new one each time.
|
|
7
|
-
*
|
|
8
|
-
* Usage:
|
|
9
|
-
* const browser = await getOrLaunchBrowserDaemon(headless);
|
|
10
|
-
* // ... use browser ...
|
|
11
|
-
* // On close: call stopDaemon() only when tab count hits zero.
|
|
12
|
-
*/
|
|
13
|
-
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { readFile, rm } from 'node:fs/promises';
|
|
14
4
|
import path from 'node:path';
|
|
15
|
-
import {
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
16
6
|
import { getAppDir } from '../paths.js';
|
|
17
|
-
import {
|
|
7
|
+
import { acquireProfileLock } from './lock.js';
|
|
8
|
+
import { isProcessAlive } from './process.js';
|
|
9
|
+
import { loadStorageState, saveStorageState } from './state.js';
|
|
18
10
|
const DAEMON_FILE = 'browser-daemon.json';
|
|
19
|
-
|
|
11
|
+
const DAEMON_LOCK_DIR = path.join(getAppDir(), 'browser-daemon-lock');
|
|
12
|
+
const STARTUP_TIMEOUT_MS = 15_000;
|
|
13
|
+
const HEALTH_TIMEOUT_MS = 3_000;
|
|
14
|
+
class BrowserDaemonHttpClient {
|
|
15
|
+
state;
|
|
16
|
+
constructor(state) {
|
|
17
|
+
this.state = state;
|
|
18
|
+
}
|
|
19
|
+
get headless() {
|
|
20
|
+
return this.state.headless;
|
|
21
|
+
}
|
|
22
|
+
async request(request) {
|
|
23
|
+
let response;
|
|
24
|
+
try {
|
|
25
|
+
response = await fetch(`http://127.0.0.1:${this.state.port}/rpc`, {
|
|
26
|
+
method: 'POST',
|
|
27
|
+
headers: {
|
|
28
|
+
Authorization: `Bearer ${this.state.token}`,
|
|
29
|
+
'Content-Type': 'application/json',
|
|
30
|
+
},
|
|
31
|
+
body: JSON.stringify(request),
|
|
32
|
+
signal: AbortSignal.timeout(60_000),
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
throw new Error(`Browser daemon request failed: ${getErrorMessage(error)}`);
|
|
37
|
+
}
|
|
38
|
+
let body;
|
|
39
|
+
try {
|
|
40
|
+
body = (await response.json());
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
throw new Error(`Browser daemon request failed with invalid response (status ${response.status}): ${getErrorMessage(error)}`);
|
|
44
|
+
}
|
|
45
|
+
if (!response.ok || !body.ok) {
|
|
46
|
+
const message = typeof body === 'object' && body && 'error' in body
|
|
47
|
+
? body.error
|
|
48
|
+
: `Browser daemon request failed with status ${response.status}`;
|
|
49
|
+
throw new Error(message);
|
|
50
|
+
}
|
|
51
|
+
return body;
|
|
52
|
+
}
|
|
53
|
+
async stop() {
|
|
54
|
+
await fetch(`http://127.0.0.1:${this.state.port}/stop`, {
|
|
55
|
+
method: 'POST',
|
|
56
|
+
headers: {
|
|
57
|
+
Authorization: `Bearer ${this.state.token}`,
|
|
58
|
+
},
|
|
59
|
+
signal: AbortSignal.timeout(HEALTH_TIMEOUT_MS),
|
|
60
|
+
}).catch(() => {
|
|
61
|
+
// Best effort; caller falls back to pid kill if needed.
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
async getLiveTabCount() {
|
|
65
|
+
let response;
|
|
66
|
+
try {
|
|
67
|
+
response = await fetch(`http://127.0.0.1:${this.state.port}/health`, {
|
|
68
|
+
method: 'GET',
|
|
69
|
+
headers: {
|
|
70
|
+
Authorization: `Bearer ${this.state.token}`,
|
|
71
|
+
},
|
|
72
|
+
signal: AbortSignal.timeout(HEALTH_TIMEOUT_MS),
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
throw new Error(`Browser daemon healthcheck failed: ${getErrorMessage(error)}`);
|
|
77
|
+
}
|
|
78
|
+
if (!response.ok) {
|
|
79
|
+
throw new Error(`Browser daemon healthcheck failed with status ${response.status}`);
|
|
80
|
+
}
|
|
81
|
+
let body;
|
|
82
|
+
try {
|
|
83
|
+
body = (await response.json());
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
throw new Error(`Browser daemon healthcheck returned invalid response (status ${response.status}): ${getErrorMessage(error)}`);
|
|
87
|
+
}
|
|
88
|
+
return body.activeTabs ?? 0;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function getErrorMessage(error) {
|
|
92
|
+
return error instanceof Error ? error.message : String(error);
|
|
93
|
+
}
|
|
94
|
+
function isDaemonCompatible(state, requestedHeadless) {
|
|
95
|
+
return !state.headless || state.headless === requestedHeadless;
|
|
96
|
+
}
|
|
97
|
+
function serializeValue(value) {
|
|
98
|
+
if (value instanceof RegExp) {
|
|
99
|
+
return {
|
|
100
|
+
__type: 'regexp',
|
|
101
|
+
source: value.source,
|
|
102
|
+
flags: value.flags,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
if (typeof value === 'function') {
|
|
106
|
+
return {
|
|
107
|
+
__type: 'function',
|
|
108
|
+
source: value.toString(),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
if (Array.isArray(value)) {
|
|
112
|
+
return value.map((entry) => serializeValue(entry));
|
|
113
|
+
}
|
|
114
|
+
if (value && typeof value === 'object') {
|
|
115
|
+
const entries = Object.entries(value).map(([key, entry]) => [key, serializeValue(entry)]);
|
|
116
|
+
return Object.fromEntries(entries);
|
|
117
|
+
}
|
|
118
|
+
return value;
|
|
119
|
+
}
|
|
120
|
+
function deserializeHandle(client, handle) {
|
|
121
|
+
switch (handle.handleType) {
|
|
122
|
+
case 'context':
|
|
123
|
+
return createRemoteContextProxy(client, handle.id);
|
|
124
|
+
case 'page':
|
|
125
|
+
return createRemotePageProxy(client, handle.id, handle.contextId ?? '', handle.url ?? 'about:blank');
|
|
126
|
+
case 'filechooser':
|
|
127
|
+
return createRemoteFileChooserProxy(client, handle.id);
|
|
128
|
+
case 'download':
|
|
129
|
+
return createRemoteDownloadProxy(client, handle.id, handle.suggestedFilename ?? '');
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
async function decodeResult(client, response, pageState) {
|
|
133
|
+
if (response.pageState && pageState) {
|
|
134
|
+
pageState.currentUrl = response.pageState.url;
|
|
135
|
+
}
|
|
136
|
+
const { result } = response;
|
|
137
|
+
if (result &&
|
|
138
|
+
typeof result === 'object' &&
|
|
139
|
+
'__handle' in result &&
|
|
140
|
+
result.__handle === true &&
|
|
141
|
+
'handleType' in result) {
|
|
142
|
+
return deserializeHandle(client, result);
|
|
143
|
+
}
|
|
144
|
+
return result;
|
|
145
|
+
}
|
|
146
|
+
function createRemoteBrowserProxy(client) {
|
|
147
|
+
return {
|
|
148
|
+
newContext: async (options) => {
|
|
149
|
+
const response = await client.request({
|
|
150
|
+
kind: 'browser',
|
|
151
|
+
method: 'newContext',
|
|
152
|
+
args: [serializeValue(options ?? {})],
|
|
153
|
+
});
|
|
154
|
+
const result = await decodeResult(client, response);
|
|
155
|
+
return result;
|
|
156
|
+
},
|
|
157
|
+
close: async () => {
|
|
158
|
+
// A remote client disconnect should not stop the shared daemon.
|
|
159
|
+
},
|
|
160
|
+
isConnected: () => true,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
function createRemoteContextProxy(client, contextId) {
|
|
164
|
+
return {
|
|
165
|
+
newPage: async () => {
|
|
166
|
+
const response = await client.request({
|
|
167
|
+
kind: 'context',
|
|
168
|
+
method: 'newPage',
|
|
169
|
+
target: { contextId },
|
|
170
|
+
});
|
|
171
|
+
const result = await decodeResult(client, response);
|
|
172
|
+
return result;
|
|
173
|
+
},
|
|
174
|
+
close: async () => {
|
|
175
|
+
await client.request({
|
|
176
|
+
kind: 'context',
|
|
177
|
+
method: 'close',
|
|
178
|
+
target: { contextId },
|
|
179
|
+
});
|
|
180
|
+
},
|
|
181
|
+
storageState: async (options) => {
|
|
182
|
+
const response = await client.request({
|
|
183
|
+
kind: 'context',
|
|
184
|
+
method: 'storageState',
|
|
185
|
+
target: { contextId },
|
|
186
|
+
args: [serializeValue(options ?? {})],
|
|
187
|
+
});
|
|
188
|
+
const result = await decodeResult(client, response);
|
|
189
|
+
return result;
|
|
190
|
+
},
|
|
191
|
+
cookies: async (urls) => {
|
|
192
|
+
const response = await client.request({
|
|
193
|
+
kind: 'context',
|
|
194
|
+
method: 'cookies',
|
|
195
|
+
target: { contextId },
|
|
196
|
+
args: [serializeValue(urls ?? [])],
|
|
197
|
+
});
|
|
198
|
+
const result = await decodeResult(client, response);
|
|
199
|
+
return (result ?? []);
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
function createRemotePageProxy(client, pageId, contextId, initialUrl) {
|
|
204
|
+
const pageState = { currentUrl: initialUrl };
|
|
205
|
+
const context = createRemoteContextProxy(client, contextId);
|
|
206
|
+
const invoke = async (method, args = []) => {
|
|
207
|
+
const response = await client.request({
|
|
208
|
+
kind: 'page',
|
|
209
|
+
method,
|
|
210
|
+
target: { pageId },
|
|
211
|
+
args: args.map((arg) => serializeValue(arg)),
|
|
212
|
+
});
|
|
213
|
+
return decodeResult(client, response, pageState);
|
|
214
|
+
};
|
|
215
|
+
return {
|
|
216
|
+
goto: async (url, options) => invoke('goto', [url, options ?? {}]),
|
|
217
|
+
waitForTimeout: async (timeout) => invoke('waitForTimeout', [timeout]),
|
|
218
|
+
waitForLoadState: async (state) => invoke('waitForLoadState', [state ?? 'load']),
|
|
219
|
+
waitForURL: async (url, options) => invoke('waitForURL', [url, options ?? {}]),
|
|
220
|
+
waitForEvent: async (event, options) => invoke('waitForEvent', [event, options ?? {}]),
|
|
221
|
+
evaluate: async (pageFunction, arg) => invoke('evaluate', [pageFunction, arg]),
|
|
222
|
+
close: async () => invoke('close'),
|
|
223
|
+
title: async () => invoke('title'),
|
|
224
|
+
url: () => pageState.currentUrl,
|
|
225
|
+
context: () => context,
|
|
226
|
+
getByRole: (role, options) => createRemoteLocatorProxy(client, pageId, pageState, [
|
|
227
|
+
{ type: 'getByRole', role, options: serializeValue(options ?? {}) },
|
|
228
|
+
]),
|
|
229
|
+
locator: (selector) => createRemoteLocatorProxy(client, pageId, pageState, [{ type: 'locator', selector }]),
|
|
230
|
+
keyboard: createRemoteKeyboardProxy(client, pageId, pageState),
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
function createRemoteKeyboardProxy(client, pageId, pageState) {
|
|
234
|
+
const invoke = async (method, args = []) => {
|
|
235
|
+
const response = await client.request({
|
|
236
|
+
kind: 'keyboard',
|
|
237
|
+
method,
|
|
238
|
+
target: { pageId },
|
|
239
|
+
args: args.map((arg) => serializeValue(arg)),
|
|
240
|
+
});
|
|
241
|
+
return decodeResult(client, response, pageState);
|
|
242
|
+
};
|
|
243
|
+
return {
|
|
244
|
+
press: async (key, options) => invoke('press', [key, options ?? {}]),
|
|
245
|
+
type: async (text, options) => invoke('type', [text, options ?? {}]),
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
function createRemoteLocatorProxy(client, pageId, pageState, steps) {
|
|
249
|
+
const invoke = async (method, args = []) => {
|
|
250
|
+
const response = await client.request({
|
|
251
|
+
kind: 'locator',
|
|
252
|
+
method,
|
|
253
|
+
target: { pageId, steps },
|
|
254
|
+
args: args.map((arg) => serializeValue(arg)),
|
|
255
|
+
});
|
|
256
|
+
return decodeResult(client, response, pageState);
|
|
257
|
+
};
|
|
258
|
+
return {
|
|
259
|
+
locator: (selector) => createRemoteLocatorProxy(client, pageId, pageState, [
|
|
260
|
+
...steps,
|
|
261
|
+
{ type: 'locator', selector },
|
|
262
|
+
]),
|
|
263
|
+
first: () => createRemoteLocatorProxy(client, pageId, pageState, [...steps, { type: 'first' }]),
|
|
264
|
+
last: () => createRemoteLocatorProxy(client, pageId, pageState, [...steps, { type: 'last' }]),
|
|
265
|
+
nth: (index) => createRemoteLocatorProxy(client, pageId, pageState, [...steps, { type: 'nth', index }]),
|
|
266
|
+
waitFor: async (options) => invoke('waitFor', [options ?? {}]),
|
|
267
|
+
isVisible: async (options) => invoke('isVisible', [options ?? {}]),
|
|
268
|
+
click: async (options) => invoke('click', [options ?? {}]),
|
|
269
|
+
fill: async (value) => invoke('fill', [value]),
|
|
270
|
+
count: async () => invoke('count'),
|
|
271
|
+
textContent: async (options) => invoke('textContent', [options ?? {}]),
|
|
272
|
+
innerHTML: async (options) => invoke('innerHTML', [options ?? {}]),
|
|
273
|
+
setInputFiles: async (files) => invoke('setInputFiles', [files]),
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
function createRemoteFileChooserProxy(client, eventId) {
|
|
277
|
+
return {
|
|
278
|
+
setFiles: async (files) => {
|
|
279
|
+
await client.request({
|
|
280
|
+
kind: 'event',
|
|
281
|
+
method: 'setFiles',
|
|
282
|
+
target: { eventId },
|
|
283
|
+
args: [serializeValue(files)],
|
|
284
|
+
});
|
|
285
|
+
},
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
function createRemoteDownloadProxy(client, eventId, suggestedFilename) {
|
|
289
|
+
return {
|
|
290
|
+
suggestedFilename: () => suggestedFilename,
|
|
291
|
+
saveAs: async (filePath) => {
|
|
292
|
+
await client.request({
|
|
293
|
+
kind: 'event',
|
|
294
|
+
method: 'saveAs',
|
|
295
|
+
target: { eventId },
|
|
296
|
+
args: [serializeValue(filePath)],
|
|
297
|
+
});
|
|
298
|
+
},
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
function getDaemonStatePathInternal() {
|
|
20
302
|
return path.join(getAppDir(), DAEMON_FILE);
|
|
21
303
|
}
|
|
304
|
+
export function getDaemonStatePath() {
|
|
305
|
+
return getDaemonStatePathInternal();
|
|
306
|
+
}
|
|
307
|
+
export async function clearDaemonState() {
|
|
308
|
+
await rm(getDaemonStatePathInternal(), { force: true }).catch(() => {
|
|
309
|
+
// ignore
|
|
310
|
+
});
|
|
311
|
+
}
|
|
22
312
|
export async function readDaemonState() {
|
|
23
313
|
try {
|
|
24
|
-
const raw = await readFile(
|
|
314
|
+
const raw = await readFile(getDaemonStatePathInternal(), 'utf-8');
|
|
25
315
|
const state = JSON.parse(raw);
|
|
26
|
-
if (
|
|
316
|
+
if (typeof state.pid !== 'number' ||
|
|
317
|
+
typeof state.port !== 'number' ||
|
|
318
|
+
typeof state.token !== 'string') {
|
|
27
319
|
return null;
|
|
320
|
+
}
|
|
28
321
|
if (!isProcessAlive(state.pid)) {
|
|
29
322
|
await clearDaemonState();
|
|
30
323
|
return null;
|
|
31
324
|
}
|
|
32
|
-
return
|
|
325
|
+
return {
|
|
326
|
+
pid: state.pid,
|
|
327
|
+
port: state.port,
|
|
328
|
+
token: state.token,
|
|
329
|
+
headless: Boolean(state.headless),
|
|
330
|
+
createdAt: typeof state.createdAt === 'string' ? state.createdAt : new Date().toISOString(),
|
|
331
|
+
};
|
|
33
332
|
}
|
|
34
333
|
catch {
|
|
35
334
|
return null;
|
|
36
335
|
}
|
|
37
336
|
}
|
|
38
|
-
async function
|
|
39
|
-
const p = getDaemonStatePath();
|
|
40
|
-
await mkdir(path.dirname(p), { recursive: true });
|
|
41
|
-
await writeFile(p, JSON.stringify(state, null, 2));
|
|
42
|
-
}
|
|
43
|
-
export async function clearDaemonState() {
|
|
337
|
+
async function pingDaemon(state) {
|
|
44
338
|
try {
|
|
45
|
-
await
|
|
339
|
+
const response = await fetch(`http://127.0.0.1:${state.port}/health`, {
|
|
340
|
+
method: 'GET',
|
|
341
|
+
headers: {
|
|
342
|
+
Authorization: `Bearer ${state.token}`,
|
|
343
|
+
},
|
|
344
|
+
signal: AbortSignal.timeout(HEALTH_TIMEOUT_MS),
|
|
345
|
+
});
|
|
346
|
+
return response.ok;
|
|
46
347
|
}
|
|
47
348
|
catch {
|
|
48
|
-
|
|
349
|
+
return false;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
async function acquireDaemonStartLock() {
|
|
353
|
+
return acquireProfileLock(DAEMON_LOCK_DIR, {
|
|
354
|
+
timeoutMs: STARTUP_TIMEOUT_MS * 2,
|
|
355
|
+
pollMs: 200,
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
function getServerEntryPoint() {
|
|
359
|
+
const currentFile = fileURLToPath(import.meta.url);
|
|
360
|
+
const currentDir = path.dirname(currentFile);
|
|
361
|
+
const jsPath = path.join(currentDir, 'server.js');
|
|
362
|
+
const tsPath = path.join(currentDir, 'server.ts');
|
|
363
|
+
const sourcePath = currentFile.endsWith('.ts') ? tsPath : jsPath;
|
|
364
|
+
if (sourcePath.endsWith('.ts')) {
|
|
365
|
+
return {
|
|
366
|
+
command: process.execPath,
|
|
367
|
+
args: ['--import', 'tsx', sourcePath],
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
return {
|
|
371
|
+
command: process.execPath,
|
|
372
|
+
args: [sourcePath],
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
async function waitForDaemonReady(headless) {
|
|
376
|
+
const deadline = Date.now() + STARTUP_TIMEOUT_MS;
|
|
377
|
+
while (Date.now() < deadline) {
|
|
378
|
+
const state = await readDaemonState();
|
|
379
|
+
if (state && isDaemonCompatible(state, headless)) {
|
|
380
|
+
const healthy = await pingDaemon(state);
|
|
381
|
+
if (healthy)
|
|
382
|
+
return state;
|
|
383
|
+
}
|
|
384
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
49
385
|
}
|
|
386
|
+
throw new Error('Timed out waiting for browser daemon to start');
|
|
387
|
+
}
|
|
388
|
+
async function startDaemon(headless) {
|
|
389
|
+
const entry = getServerEntryPoint();
|
|
390
|
+
const child = spawn(entry.command, entry.args, {
|
|
391
|
+
detached: true,
|
|
392
|
+
stdio: 'ignore',
|
|
393
|
+
env: {
|
|
394
|
+
...process.env,
|
|
395
|
+
TEN_X_CHAT_BROWSER_HEADLESS: headless ? '1' : '0',
|
|
396
|
+
TEN_X_CHAT_BROWSER_STATE_FILE: getDaemonStatePathInternal(),
|
|
397
|
+
},
|
|
398
|
+
});
|
|
399
|
+
child.unref();
|
|
400
|
+
return waitForDaemonReady(headless);
|
|
401
|
+
}
|
|
402
|
+
async function ensureDaemonState(headless) {
|
|
403
|
+
const existing = await readDaemonState();
|
|
404
|
+
if (existing && isDaemonCompatible(existing, headless) && (await pingDaemon(existing))) {
|
|
405
|
+
return existing;
|
|
406
|
+
}
|
|
407
|
+
const lock = await acquireDaemonStartLock();
|
|
408
|
+
try {
|
|
409
|
+
const current = await readDaemonState();
|
|
410
|
+
if (current) {
|
|
411
|
+
const healthy = await pingDaemon(current);
|
|
412
|
+
if (healthy && isDaemonCompatible(current, headless)) {
|
|
413
|
+
return current;
|
|
414
|
+
}
|
|
415
|
+
if (healthy) {
|
|
416
|
+
await new BrowserDaemonHttpClient(current).stop();
|
|
417
|
+
if (isProcessAlive(current.pid)) {
|
|
418
|
+
try {
|
|
419
|
+
process.kill(current.pid, 'SIGTERM');
|
|
420
|
+
}
|
|
421
|
+
catch {
|
|
422
|
+
// already gone
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
await clearDaemonState();
|
|
427
|
+
}
|
|
428
|
+
return startDaemon(headless);
|
|
429
|
+
}
|
|
430
|
+
finally {
|
|
431
|
+
await lock.release();
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
async function getDaemonClient(headless = true) {
|
|
435
|
+
const state = await ensureDaemonState(headless);
|
|
436
|
+
return new BrowserDaemonHttpClient(state);
|
|
50
437
|
}
|
|
51
|
-
/**
|
|
52
|
-
* Stop the daemon browser by killing its server process.
|
|
53
|
-
* Safe to call even if daemon is not running.
|
|
54
|
-
*/
|
|
55
438
|
export async function stopDaemon() {
|
|
56
439
|
const state = await readDaemonState();
|
|
57
|
-
if (state)
|
|
440
|
+
if (!state)
|
|
441
|
+
return;
|
|
442
|
+
const healthy = await pingDaemon(state);
|
|
443
|
+
if (!healthy) {
|
|
444
|
+
await clearDaemonState();
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
const client = new BrowserDaemonHttpClient(state);
|
|
448
|
+
await client.stop();
|
|
449
|
+
if (isProcessAlive(state.pid)) {
|
|
58
450
|
try {
|
|
59
451
|
process.kill(state.pid, 'SIGTERM');
|
|
60
452
|
}
|
|
61
453
|
catch {
|
|
62
|
-
// already
|
|
454
|
+
// already gone
|
|
63
455
|
}
|
|
64
|
-
await clearDaemonState();
|
|
65
456
|
}
|
|
457
|
+
await clearDaemonState();
|
|
66
458
|
}
|
|
67
|
-
/**
|
|
68
|
-
* Get an existing browser daemon (reconnect) or launch a new one.
|
|
69
|
-
* Returns a Playwright Browser connected to the shared server process.
|
|
70
|
-
*
|
|
71
|
-
* The returned Browser is a client connection — calling browser.close() on it
|
|
72
|
-
* disconnects this client but does NOT stop the server. Call stopDaemon()
|
|
73
|
-
* when you actually want to shut the server down (i.e. last tab closed).
|
|
74
|
-
*/
|
|
75
459
|
export async function getOrLaunchBrowserDaemon(headless = true) {
|
|
76
|
-
const
|
|
77
|
-
|
|
460
|
+
const client = await getDaemonClient(headless);
|
|
461
|
+
return createRemoteBrowserProxy(client);
|
|
462
|
+
}
|
|
463
|
+
export async function launchSharedBrowserSession(opts) {
|
|
464
|
+
const { headless = true, url } = opts;
|
|
465
|
+
const client = await getDaemonClient(headless);
|
|
466
|
+
const storageStatePath = await loadStorageState();
|
|
467
|
+
const browser = createRemoteBrowserProxy(client);
|
|
468
|
+
const context = (await browser.newContext({
|
|
469
|
+
viewport: { width: 1280, height: 900 },
|
|
470
|
+
...(storageStatePath ? { storageState: storageStatePath } : {}),
|
|
471
|
+
}));
|
|
472
|
+
const page = (await context.newPage());
|
|
473
|
+
try {
|
|
474
|
+
if (url) {
|
|
475
|
+
await page.goto(url, { waitUntil: 'domcontentloaded' });
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
catch (error) {
|
|
479
|
+
await context.close().catch(() => {
|
|
480
|
+
// best effort
|
|
481
|
+
});
|
|
482
|
+
throw error;
|
|
483
|
+
}
|
|
484
|
+
const close = async () => {
|
|
78
485
|
try {
|
|
79
|
-
|
|
80
|
-
return browser;
|
|
486
|
+
await saveStorageState(context);
|
|
81
487
|
}
|
|
82
488
|
catch {
|
|
83
|
-
//
|
|
84
|
-
await clearDaemonState();
|
|
489
|
+
// best effort
|
|
85
490
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
491
|
+
await page.close().catch(() => {
|
|
492
|
+
// already closed
|
|
493
|
+
});
|
|
494
|
+
await context.close().catch(() => {
|
|
495
|
+
// already closed
|
|
496
|
+
});
|
|
497
|
+
};
|
|
498
|
+
return { context, page, close };
|
|
499
|
+
}
|
|
500
|
+
export async function registerDaemonTab() {
|
|
501
|
+
const client = await getDaemonClient(true);
|
|
502
|
+
const response = await client.request({
|
|
503
|
+
kind: 'tabs',
|
|
504
|
+
method: 'register',
|
|
505
|
+
args: [serializeValue(randomUUID())],
|
|
98
506
|
});
|
|
99
|
-
|
|
507
|
+
const result = await decodeResult(client, response);
|
|
508
|
+
return typeof result === 'string' ? result : '';
|
|
509
|
+
}
|
|
510
|
+
export async function unregisterDaemonTab(tabKey) {
|
|
511
|
+
const state = await readDaemonState();
|
|
512
|
+
if (!state)
|
|
513
|
+
return 0;
|
|
514
|
+
const client = new BrowserDaemonHttpClient(state);
|
|
515
|
+
try {
|
|
516
|
+
const response = await client.request({
|
|
517
|
+
kind: 'tabs',
|
|
518
|
+
method: 'unregister',
|
|
519
|
+
args: [serializeValue(tabKey)],
|
|
520
|
+
});
|
|
521
|
+
const result = await decodeResult(client, response);
|
|
522
|
+
return typeof result === 'number' ? result : 0;
|
|
523
|
+
}
|
|
524
|
+
catch {
|
|
525
|
+
return 0;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
export async function readLiveTabCount() {
|
|
529
|
+
const state = await readDaemonState();
|
|
530
|
+
if (!state)
|
|
531
|
+
return 0;
|
|
532
|
+
const client = new BrowserDaemonHttpClient(state);
|
|
533
|
+
try {
|
|
534
|
+
return await client.getLiveTabCount();
|
|
535
|
+
}
|
|
536
|
+
catch {
|
|
537
|
+
return 0;
|
|
538
|
+
}
|
|
100
539
|
}
|
|
101
540
|
//# sourceMappingURL=daemon.js.map
|