10x-chat 0.8.1 → 0.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.
Files changed (45) hide show
  1. package/README.md +1 -1
  2. package/dist/browser/daemon.d.ts +22 -29
  3. package/dist/browser/daemon.d.ts.map +1 -1
  4. package/dist/browser/daemon.js +500 -61
  5. package/dist/browser/daemon.js.map +1 -1
  6. package/dist/browser/manager.d.ts.map +1 -1
  7. package/dist/browser/manager.js +8 -53
  8. package/dist/browser/manager.js.map +1 -1
  9. package/dist/browser/page-utils.d.ts +4 -0
  10. package/dist/browser/page-utils.d.ts.map +1 -0
  11. package/dist/browser/page-utils.js +37 -0
  12. package/dist/browser/page-utils.js.map +1 -0
  13. package/dist/browser/server.d.ts +2 -0
  14. package/dist/browser/server.d.ts.map +1 -0
  15. package/dist/browser/server.js +510 -0
  16. package/dist/browser/server.js.map +1 -0
  17. package/dist/browser/tabs.d.ts +2 -16
  18. package/dist/browser/tabs.d.ts.map +1 -1
  19. package/dist/browser/tabs.js +6 -80
  20. package/dist/browser/tabs.js.map +1 -1
  21. package/dist/core/image-orchestrator.d.ts.map +1 -1
  22. package/dist/core/image-orchestrator.js +11 -12
  23. package/dist/core/image-orchestrator.js.map +1 -1
  24. package/dist/core/research-orchestrator.d.ts.map +1 -1
  25. package/dist/core/research-orchestrator.js +110 -29
  26. package/dist/core/research-orchestrator.js.map +1 -1
  27. package/dist/notebooklm/rpc/decoder.d.ts +1 -1
  28. package/dist/notebooklm/rpc/decoder.d.ts.map +1 -1
  29. package/dist/notebooklm/rpc/decoder.js +1 -1
  30. package/dist/notebooklm/rpc/decoder.js.map +1 -1
  31. package/dist/notebooklm/types.d.ts +1 -2
  32. package/dist/notebooklm/types.d.ts.map +1 -1
  33. package/dist/notebooklm/types.js +1 -2
  34. package/dist/notebooklm/types.js.map +1 -1
  35. package/dist/providers/chatgpt.d.ts.map +1 -1
  36. package/dist/providers/chatgpt.js +14 -8
  37. package/dist/providers/chatgpt.js.map +1 -1
  38. package/dist/providers/grok.d.ts.map +1 -1
  39. package/dist/providers/grok.js +2 -3
  40. package/dist/providers/grok.js.map +1 -1
  41. package/dist/providers/perplexity.d.ts.map +1 -1
  42. package/dist/providers/perplexity.js +2 -5
  43. package/dist/providers/perplexity.js.map +1 -1
  44. package/package.json +4 -4
  45. package/skills/10x-chat/SKILL.md +105 -50
package/README.md CHANGED
@@ -11,7 +11,7 @@
11
11
  Paste this into your [OpenClaw](https://openclaw.ai) chat to install as a skill:
12
12
 
13
13
  ```
14
- https://raw.githubusercontent.com/RealMikeChong/10x-chat/refs/heads/main/skills/10x-chat/SKILL.md
14
+ https://raw.githubusercontent.com/MikeChongCan/10x-chat/refs/heads/main/skills/10x-chat/SKILL.md
15
15
  ```
16
16
 
17
17
  ## Quick Start
@@ -1,39 +1,32 @@
1
- /**
2
- * Browser daemon — shared long-running Chromium process.
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 { 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
- /** Playwright WS endpoint for chromium.connect(). */
18
- wsEndpoint: string;
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":"AAAA;;;;;;;;;;;GAWG;AAIH,OAAO,EAAE,KAAK,OAAO,EAAY,MAAM,YAAY,CAAC;AAMpD,MAAM,WAAW,WAAW;IAC1B,0CAA0C;IAC1C,GAAG,EAAE,MAAM,CAAC;IACZ,qDAAqD;IACrD,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,OAAO,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C;AAED,wBAAsB,eAAe,IAAI,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAanE;AAQD,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC,CAMtD;AAED;;;GAGG;AACH,wBAAsB,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CAUhD;AAED;;;;;;;GAOG;AACH,wBAAsB,wBAAwB,CAAC,QAAQ,UAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CA8BhF"}
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"}
@@ -1,101 +1,540 @@
1
- /**
2
- * Browser daemon shared long-running Chromium process.
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 { chromium } from 'playwright';
5
+ import { fileURLToPath } from 'node:url';
16
6
  import { getAppDir } from '../paths.js';
17
- import { CHROMIUM_ARGS, isProcessAlive } from './process.js';
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
- export function getDaemonStatePath() {
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(getDaemonStatePath(), 'utf-8');
314
+ const raw = await readFile(getDaemonStatePathInternal(), 'utf-8');
25
315
  const state = JSON.parse(raw);
26
- if (!state.pid || !state.wsEndpoint)
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 state;
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 writeDaemonState(state) {
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 rm(getDaemonStatePath(), { force: true });
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
- // ignore
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 dead
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 existing = await readDaemonState();
77
- if (existing) {
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
- const browser = await chromium.connect(existing.wsEndpoint, { timeout: 5_000 });
80
- return browser;
486
+ await saveStorageState(context);
81
487
  }
82
488
  catch {
83
- // Server died without cleaning up state
84
- await clearDaemonState();
489
+ // best effort
85
490
  }
86
- }
87
- // Launch a new browser server
88
- const server = await chromium.launchServer({
89
- headless,
90
- args: CHROMIUM_ARGS,
91
- });
92
- const pid = server.process().pid ?? -1;
93
- const wsEndpoint = server.wsEndpoint();
94
- await writeDaemonState({ pid, wsEndpoint, headless, createdAt: new Date().toISOString() });
95
- // Clean up state file if server exits unexpectedly
96
- server.process().once('exit', () => {
97
- void clearDaemonState();
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
- return chromium.connect(wsEndpoint);
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