@jshookmcp/jshook 0.1.5 → 0.1.7

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 (61) hide show
  1. package/README.md +72 -40
  2. package/README.zh.md +77 -40
  3. package/dist/constants.d.ts +1 -0
  4. package/dist/constants.js +13 -1
  5. package/dist/index.js +0 -0
  6. package/dist/modules/analyzer/IntelligentAnalyzer.js +19 -11
  7. package/dist/modules/browser/BrowserModeManager.d.ts +5 -0
  8. package/dist/modules/browser/BrowserModeManager.js +96 -10
  9. package/dist/modules/browser/CamoufoxBrowserManager.d.ts +4 -0
  10. package/dist/modules/browser/CamoufoxBrowserManager.js +64 -3
  11. package/dist/modules/browser/TabRegistry.js +3 -2
  12. package/dist/modules/browser/UnifiedBrowserManager.d.ts +5 -0
  13. package/dist/modules/browser/UnifiedBrowserManager.js +62 -9
  14. package/dist/modules/debugger/DebuggerSessionManager.d.ts +4 -0
  15. package/dist/modules/debugger/DebuggerSessionManager.js +29 -19
  16. package/dist/modules/debugger/ScriptManager.impl.class.d.ts +4 -0
  17. package/dist/modules/debugger/ScriptManager.impl.class.js +46 -21
  18. package/dist/modules/emulator/EnvironmentEmulator.js +2 -2
  19. package/dist/modules/monitor/NetworkMonitor.impl.d.ts +1 -0
  20. package/dist/modules/monitor/NetworkMonitor.impl.js +22 -15
  21. package/dist/modules/monitor/PerformanceMonitor.js +64 -32
  22. package/dist/modules/process/LinuxProcessManager.d.ts +3 -1
  23. package/dist/modules/process/LinuxProcessManager.js +7 -3
  24. package/dist/modules/process/MacProcessManager.d.ts +3 -1
  25. package/dist/modules/process/MacProcessManager.js +7 -3
  26. package/dist/modules/process/ProcessManager.impl.d.ts +5 -1
  27. package/dist/modules/process/ProcessManager.impl.js +54 -13
  28. package/dist/modules/process/index.d.ts +3 -1
  29. package/dist/modules/process/index.js +2 -2
  30. package/dist/modules/process/memory/AuditTrail.d.ts +25 -0
  31. package/dist/modules/process/memory/AuditTrail.js +44 -0
  32. package/dist/modules/process/memory/linux/mapsParser.d.ts +16 -0
  33. package/dist/modules/process/memory/linux/mapsParser.js +28 -0
  34. package/dist/modules/process/memory/regions.enumerate.js +45 -1
  35. package/dist/modules/process/memory/regions.protection.js +48 -2
  36. package/dist/modules/process/memory/scanner.d.ts +4 -1
  37. package/dist/modules/process/memory/scanner.js +225 -24
  38. package/dist/native/NativeMemoryManager.impl.d.ts +4 -0
  39. package/dist/native/NativeMemoryManager.impl.js +72 -24
  40. package/dist/native/NativeMemoryManager.utils.d.ts +1 -0
  41. package/dist/native/NativeMemoryManager.utils.js +44 -1
  42. package/dist/server/MCPServer.search.d.ts +3 -0
  43. package/dist/server/MCPServer.search.js +21 -2
  44. package/dist/server/ToolCallContextGuard.d.ts +2 -0
  45. package/dist/server/ToolCallContextGuard.js +29 -14
  46. package/dist/server/ToolSearch.js +11 -5
  47. package/dist/server/domains/browser/handlers/tab-workflow.js +6 -4
  48. package/dist/server/domains/maintenance/handlers.extensions.js +46 -26
  49. package/dist/server/domains/process/definitions.js +20 -7
  50. package/dist/server/domains/process/handlers.impl.core.runtime.base.d.ts +35 -0
  51. package/dist/server/domains/process/handlers.impl.core.runtime.base.js +107 -1
  52. package/dist/server/domains/process/handlers.impl.core.runtime.inject.js +111 -2
  53. package/dist/server/domains/process/handlers.impl.core.runtime.memory.d.ts +9 -0
  54. package/dist/server/domains/process/handlers.impl.core.runtime.memory.js +282 -31
  55. package/dist/server/domains/process/manifest.js +1 -0
  56. package/dist/server/domains/workflow/handlers.impl.workflow-api.js +14 -4
  57. package/dist/server/registry/discovery.js +17 -12
  58. package/dist/server/registry/index.js +10 -2
  59. package/dist/utils/TokenBudgetManager.d.ts +1 -0
  60. package/dist/utils/TokenBudgetManager.js +22 -0
  61. package/package.json +28 -41
@@ -7,6 +7,8 @@ export class BrowserModeManager {
7
7
  browser = null;
8
8
  currentPage = null;
9
9
  isHeadless = true;
10
+ isClosing = false;
11
+ launchPromise;
10
12
  config;
11
13
  captchaDetector;
12
14
  launchOptions;
@@ -24,6 +26,27 @@ export class BrowserModeManager {
24
26
  this.launchOptions = launchOptions;
25
27
  }
26
28
  async launch() {
29
+ if (this.browser?.isConnected()) {
30
+ return this.browser;
31
+ }
32
+ if (this.isClosing) {
33
+ throw new Error('Cannot launch browser while closing');
34
+ }
35
+ if (this.launchPromise) {
36
+ return this.launchPromise;
37
+ }
38
+ const launchPromise = this.doLaunch();
39
+ this.launchPromise = launchPromise;
40
+ try {
41
+ return await launchPromise;
42
+ }
43
+ finally {
44
+ if (this.launchPromise === launchPromise) {
45
+ this.launchPromise = undefined;
46
+ }
47
+ }
48
+ }
49
+ async doLaunch() {
27
50
  const headlessMode = this.isHeadless;
28
51
  const executablePath = this.resolveExecutablePath();
29
52
  logger.info(`Launching browser (${headlessMode ? 'headless' : 'headed'} mode)...`);
@@ -43,7 +66,14 @@ export class BrowserModeManager {
43
66
  if (executablePath) {
44
67
  options.executablePath = executablePath;
45
68
  }
46
- this.browser = await puppeteer.launch(options);
69
+ const browser = await puppeteer.launch(options);
70
+ if (this.isClosing) {
71
+ await browser.close().catch(error => {
72
+ logger.warn('Failed to close browser launched during shutdown', error);
73
+ });
74
+ throw new Error('Browser launch aborted because close was requested');
75
+ }
76
+ this.browser = browser;
47
77
  logger.info('Browser launched successfully');
48
78
  return this.browser;
49
79
  }
@@ -64,10 +94,8 @@ export class BrowserModeManager {
64
94
  return undefined;
65
95
  }
66
96
  async newPage() {
67
- if (!this.browser) {
68
- await this.launch();
69
- }
70
- const page = await this.browser.newPage();
97
+ const browser = this.browser?.isConnected() ? this.browser : await this.launch();
98
+ const page = await browser.newPage();
71
99
  this.currentPage = page;
72
100
  await this.injectAntiDetectionScripts(page);
73
101
  if (this.sessionData.cookies && this.sessionData.cookies.length > 0) {
@@ -75,6 +103,20 @@ export class BrowserModeManager {
75
103
  }
76
104
  return page;
77
105
  }
106
+ async finalizeClose() {
107
+ try {
108
+ const browser = this.browser;
109
+ this.browser = null;
110
+ this.currentPage = null;
111
+ if (browser) {
112
+ await browser.close();
113
+ logger.info('Browser closed');
114
+ }
115
+ }
116
+ finally {
117
+ this.isClosing = false;
118
+ }
119
+ }
78
120
  async goto(url, page) {
79
121
  const targetPage = page || this.currentPage;
80
122
  if (!targetPage) {
@@ -111,6 +153,10 @@ export class BrowserModeManager {
111
153
  await this.launch();
112
154
  const newPage = await this.newPage();
113
155
  await newPage.goto(url, { waitUntil: 'networkidle2' });
156
+ await this.restoreSessionData(newPage);
157
+ if (this.sessionData.localStorage || this.sessionData.sessionStorage) {
158
+ await newPage.reload({ waitUntil: 'networkidle2' });
159
+ }
114
160
  this.showCaptchaPrompt(captchaInfo);
115
161
  const completed = await this.captchaDetector.waitForCompletion(newPage, this.config.captchaTimeout);
116
162
  if (completed) {
@@ -147,7 +193,10 @@ export class BrowserModeManager {
147
193
  }
148
194
  }
149
195
  async saveSessionData(page) {
196
+ this.sessionData = {};
150
197
  try {
198
+ const url = page.url();
199
+ this.sessionData.origin = url !== 'about:blank' ? new URL(url).origin : undefined;
151
200
  this.sessionData.cookies = await page.cookies();
152
201
  const storageData = await page.evaluate(() => {
153
202
  const local = {};
@@ -174,6 +223,37 @@ export class BrowserModeManager {
174
223
  logger.error('Failed to capture session data before mode switch', error);
175
224
  }
176
225
  }
226
+ async restoreSessionData(page) {
227
+ try {
228
+ const currentUrl = page.url();
229
+ const currentOrigin = currentUrl !== 'about:blank' ? new URL(currentUrl).origin : undefined;
230
+ if (this.sessionData.origin && currentOrigin && this.sessionData.origin !== currentOrigin) {
231
+ logger.warn(`Origin mismatch: session data from ${this.sessionData.origin} cannot be restored to ${currentOrigin}. ` +
232
+ 'This prevents cross-origin data leakage.');
233
+ return;
234
+ }
235
+ if (this.sessionData.localStorage || this.sessionData.sessionStorage) {
236
+ await page.evaluate((data) => {
237
+ const restoreStorage = (storage, items) => {
238
+ if (items) {
239
+ for (const [key, value] of Object.entries(items)) {
240
+ storage.setItem(key, value);
241
+ }
242
+ }
243
+ };
244
+ restoreStorage(localStorage, data.local);
245
+ restoreStorage(sessionStorage, data.session);
246
+ }, {
247
+ local: this.sessionData.localStorage,
248
+ session: this.sessionData.sessionStorage
249
+ });
250
+ logger.info('Session storage data restored');
251
+ }
252
+ }
253
+ catch (error) {
254
+ logger.error('Failed to restore session storage data', error);
255
+ }
256
+ }
177
257
  async injectAntiDetectionScripts(page) {
178
258
  await page.evaluateOnNewDocument(() => {
179
259
  Object.defineProperty(navigator, 'webdriver', {
@@ -265,12 +345,18 @@ export class BrowserModeManager {
265
345
  logger.info('Injected anti-detection scripts');
266
346
  }
267
347
  async close() {
268
- if (this.browser) {
269
- await this.browser.close();
270
- this.browser = null;
271
- this.currentPage = null;
272
- logger.info('Browser closed');
348
+ this.sessionData = {};
349
+ this.isClosing = true;
350
+ const pendingLaunch = this.launchPromise;
351
+ if (pendingLaunch) {
352
+ void pendingLaunch
353
+ .catch(() => undefined)
354
+ .finally(() => {
355
+ void this.finalizeClose();
356
+ });
357
+ return;
273
358
  }
359
+ await this.finalizeClose();
274
360
  }
275
361
  getBrowser() {
276
362
  return this.browser;
@@ -30,11 +30,15 @@ export declare class CamoufoxBrowserManager {
30
30
  private browser;
31
31
  private browserServer;
32
32
  private config;
33
+ private isClosing;
34
+ private launchPromise?;
33
35
  constructor(config?: CamoufoxBrowserConfig);
34
36
  launch(): Promise<CamoufoxBrowserLike>;
37
+ private doLaunch;
35
38
  newPage(): Promise<CamoufoxPageLike>;
36
39
  goto(url: string, page?: CamoufoxPageLike): Promise<CamoufoxPageLike>;
37
40
  close(): Promise<void>;
41
+ private finalizeClose;
38
42
  launchAsServer(port?: number, ws_path?: string): Promise<string>;
39
43
  connectToServer(wsEndpoint: string): Promise<CamoufoxBrowserLike>;
40
44
  closeBrowserServer(): Promise<void>;
@@ -4,6 +4,8 @@ export class CamoufoxBrowserManager {
4
4
  browser = null;
5
5
  browserServer = null;
6
6
  config;
7
+ isClosing = false;
8
+ launchPromise;
7
9
  constructor(config = {}) {
8
10
  this.config = {
9
11
  os: config.os ?? 'windows',
@@ -16,6 +18,29 @@ export class CamoufoxBrowserManager {
16
18
  };
17
19
  }
18
20
  async launch() {
21
+ if (this.browser?.isConnected()) {
22
+ return this.browser;
23
+ }
24
+ if (this.isClosing) {
25
+ throw new Error('Cannot launch browser while closing');
26
+ }
27
+ if (this.launchPromise) {
28
+ return this.launchPromise;
29
+ }
30
+ this.launchPromise = this.doLaunch();
31
+ try {
32
+ return await this.launchPromise;
33
+ }
34
+ finally {
35
+ this.launchPromise = undefined;
36
+ }
37
+ }
38
+ async doLaunch() {
39
+ if (this.browser) {
40
+ logger.info('Closing existing Camoufox browser before relaunch');
41
+ await this.browser.close().catch(err => logger.warn('Failed to close previous browser:', err));
42
+ this.browser = null;
43
+ }
19
44
  logger.info(`Launching Camoufox (Firefox) [os=${this.config.os}, headless=${this.config.headless}]...`);
20
45
  let Camoufox;
21
46
  try {
@@ -33,6 +58,13 @@ export class CamoufoxBrowserManager {
33
58
  block_images: this.config.blockImages,
34
59
  block_webrtc: this.config.blockWebrtc,
35
60
  }));
61
+ if (this.isClosing) {
62
+ await this.browser.close().catch(error => {
63
+ logger.warn('Failed to close Camoufox browser launched during shutdown:', error);
64
+ });
65
+ this.browser = null;
66
+ throw new Error('Camoufox launch aborted because close was requested');
67
+ }
36
68
  logger.info('Camoufox browser launched');
37
69
  return this.browser;
38
70
  }
@@ -51,10 +83,29 @@ export class CamoufoxBrowserManager {
51
83
  return targetPage;
52
84
  }
53
85
  async close() {
54
- if (this.browser) {
55
- await this.browser.close();
86
+ this.isClosing = true;
87
+ const pendingLaunch = this.launchPromise;
88
+ if (pendingLaunch) {
89
+ void pendingLaunch
90
+ .catch(() => undefined)
91
+ .finally(() => {
92
+ void this.finalizeClose();
93
+ });
94
+ return;
95
+ }
96
+ await this.finalizeClose();
97
+ }
98
+ async finalizeClose() {
99
+ try {
100
+ const browser = this.browser;
56
101
  this.browser = null;
57
- logger.info('Camoufox browser closed');
102
+ if (browser) {
103
+ await browser.close();
104
+ logger.info('Camoufox browser closed');
105
+ }
106
+ }
107
+ finally {
108
+ this.isClosing = false;
58
109
  }
59
110
  }
60
111
  async launchAsServer(port, ws_path) {
@@ -77,6 +128,11 @@ export class CamoufoxBrowserManager {
77
128
  port,
78
129
  ws_path,
79
130
  };
131
+ if (this.browserServer) {
132
+ logger.info('Closing existing Camoufox server before relaunch');
133
+ await this.browserServer.close().catch(err => logger.warn('Failed to close previous server:', err));
134
+ this.browserServer = null;
135
+ }
80
136
  this.browserServer = await launchServer(serverOptions);
81
137
  const endpoint = this.browserServer.wsEndpoint();
82
138
  logger.info(`Camoufox server listening on: ${endpoint}`);
@@ -84,6 +140,11 @@ export class CamoufoxBrowserManager {
84
140
  }
85
141
  async connectToServer(wsEndpoint) {
86
142
  logger.info(`Connecting to Camoufox server: ${wsEndpoint}`);
143
+ if (this.browser) {
144
+ logger.info('Disconnecting existing browser before new connection');
145
+ await this.browser.close().catch(err => logger.warn('Failed to close previous browser:', err));
146
+ this.browser = null;
147
+ }
87
148
  const playwrightModule = await import('playwright-core');
88
149
  const firefox = playwrightModule.firefox;
89
150
  this.browser = (await firefox.connect(wsEndpoint));
@@ -113,7 +113,8 @@ export class TabRegistry {
113
113
  return null;
114
114
  }
115
115
  setCurrentPageId(pageId) {
116
- if (!this.tabsById.has(pageId))
116
+ const entry = this.tabsById.get(pageId);
117
+ if (!entry || entry.stale)
117
118
  return false;
118
119
  this.currentPageId = pageId;
119
120
  return true;
@@ -167,7 +168,7 @@ export class TabRegistry {
167
168
  url: current?.meta.url ?? null,
168
169
  title: current?.meta.title ?? null,
169
170
  tabIndex: current?.meta.index ?? null,
170
- pageId: this.currentPageId,
171
+ pageId: current ? this.currentPageId : null,
171
172
  };
172
173
  }
173
174
  listTabs() {
@@ -46,10 +46,15 @@ export declare class UnifiedBrowserManager implements IBrowserManager {
46
46
  private camoufoxManager;
47
47
  private browserDiscovery;
48
48
  private activePage;
49
+ private chromeLaunchPromise?;
50
+ private camoufoxLaunchPromise?;
51
+ private isClosing;
49
52
  constructor(config?: UnifiedBrowserConfig);
50
53
  launch(): Promise<PuppeteerBrowser | CamoufoxBrowserLike>;
51
54
  private launchChrome;
55
+ private doLaunchChrome;
52
56
  private launchCamoufox;
57
+ private doLaunchCamoufox;
53
58
  connect(wsEndpoint: string): Promise<PuppeteerBrowser | CamoufoxBrowserLike>;
54
59
  private connectChrome;
55
60
  private connectCamoufox;
@@ -9,6 +9,9 @@ export class UnifiedBrowserManager {
9
9
  camoufoxManager = null;
10
10
  browserDiscovery;
11
11
  activePage = null;
12
+ chromeLaunchPromise;
13
+ camoufoxLaunchPromise;
14
+ isClosing = false;
12
15
  constructor(config = {}) {
13
16
  this.config = config;
14
17
  this.driver = config.driver ?? 'chrome';
@@ -21,6 +24,25 @@ export class UnifiedBrowserManager {
21
24
  return this.launchChrome();
22
25
  }
23
26
  async launchChrome() {
27
+ if (this.isClosing) {
28
+ throw new Error('Cannot launch browser while closing');
29
+ }
30
+ const existingBrowser = this.chromeManager?.getBrowser();
31
+ if (existingBrowser?.isConnected()) {
32
+ return existingBrowser;
33
+ }
34
+ if (this.chromeLaunchPromise) {
35
+ return this.chromeLaunchPromise;
36
+ }
37
+ this.chromeLaunchPromise = this.doLaunchChrome();
38
+ try {
39
+ return await this.chromeLaunchPromise;
40
+ }
41
+ finally {
42
+ this.chromeLaunchPromise = undefined;
43
+ }
44
+ }
45
+ async doLaunchChrome() {
24
46
  logger.info(`Launching Chrome [headless=${this.config.headless ?? true}]...`);
25
47
  const modeConfig = {
26
48
  autoDetectCaptcha: this.config.autoDetectCaptcha,
@@ -46,6 +68,25 @@ export class UnifiedBrowserManager {
46
68
  return browser;
47
69
  }
48
70
  async launchCamoufox() {
71
+ if (this.isClosing) {
72
+ throw new Error('Cannot launch browser while closing');
73
+ }
74
+ const existingBrowser = this.camoufoxManager?.getBrowser();
75
+ if (existingBrowser?.isConnected()) {
76
+ return existingBrowser;
77
+ }
78
+ if (this.camoufoxLaunchPromise) {
79
+ return this.camoufoxLaunchPromise;
80
+ }
81
+ this.camoufoxLaunchPromise = this.doLaunchCamoufox();
82
+ try {
83
+ return await this.camoufoxLaunchPromise;
84
+ }
85
+ finally {
86
+ this.camoufoxLaunchPromise = undefined;
87
+ }
88
+ }
89
+ async doLaunchCamoufox() {
49
90
  const headless = this.normalizeCamoufoxHeadless();
50
91
  logger.info(`Launching Camoufox (Firefox) [os=${this.config.os ?? 'windows'}, headless=${headless}]...`);
51
92
  const camoufoxConfig = {
@@ -114,18 +155,30 @@ export class UnifiedBrowserManager {
114
155
  return this.chromeManager.goto(url, targetPage);
115
156
  }
116
157
  async close() {
117
- if (this.driver === 'camoufox' && this.camoufoxManager) {
118
- await this.camoufoxManager.close();
158
+ this.isClosing = true;
159
+ try {
160
+ const camoufoxManager = this.camoufoxManager;
161
+ const chromeManager = this.chromeManager;
119
162
  this.camoufoxManager = null;
120
- this.activePage = null;
121
- logger.info('Camoufox browser closed');
122
- return;
123
- }
124
- if (this.chromeManager) {
125
- await this.chromeManager.close();
126
163
  this.chromeManager = null;
164
+ this.chromeLaunchPromise = undefined;
165
+ this.camoufoxLaunchPromise = undefined;
127
166
  this.activePage = null;
128
- logger.info('Chrome browser closed');
167
+ const closeTasks = [];
168
+ if (camoufoxManager) {
169
+ closeTasks.push(camoufoxManager.close().then(() => {
170
+ logger.info('Camoufox browser closed');
171
+ }));
172
+ }
173
+ if (chromeManager) {
174
+ closeTasks.push(chromeManager.close().then(() => {
175
+ logger.info('Chrome browser closed');
176
+ }));
177
+ }
178
+ await Promise.all(closeTasks);
179
+ }
180
+ finally {
181
+ this.isClosing = false;
129
182
  }
130
183
  }
131
184
  getBrowser() {
@@ -7,7 +7,11 @@ type SavedDebuggerSessionSummary = {
7
7
  };
8
8
  export declare class DebuggerSessionManager {
9
9
  private debuggerManager;
10
+ private readonly SESSION_IMPORT_BATCH_SIZE;
11
+ private readonly SESSION_FILE_READ_BATCH_SIZE;
10
12
  constructor(debuggerManager: DebuggerManager);
13
+ private processInBatches;
14
+ private readSessionFile;
11
15
  private validateFilePath;
12
16
  exportSession(metadata?: DebuggerSession['metadata']): DebuggerSession;
13
17
  saveSession(filePath?: string, metadata?: DebuggerSession['metadata']): Promise<string>;
@@ -4,9 +4,20 @@ import * as path from 'path';
4
4
  import { logger } from '../../utils/logger.js';
5
5
  export class DebuggerSessionManager {
6
6
  debuggerManager;
7
+ SESSION_IMPORT_BATCH_SIZE = 8;
8
+ SESSION_FILE_READ_BATCH_SIZE = 8;
7
9
  constructor(debuggerManager) {
8
10
  this.debuggerManager = debuggerManager;
9
11
  }
12
+ async processInBatches(items, batchSize, worker) {
13
+ for (let i = 0; i < items.length; i += batchSize) {
14
+ const batch = items.slice(i, i + batchSize);
15
+ await Promise.all(batch.map((item) => worker(item)));
16
+ }
17
+ }
18
+ async readSessionFile(filePath) {
19
+ return fs.readFile(filePath, 'utf-8');
20
+ }
10
21
  async validateFilePath(filePath) {
11
22
  const resolved = path.resolve(filePath);
12
23
  const cwd = await fs.realpath(process.cwd());
@@ -70,7 +81,7 @@ export class DebuggerSessionManager {
70
81
  }
71
82
  async loadSessionFromFile(filePath) {
72
83
  const resolvedPath = await this.validateFilePath(filePath);
73
- const content = await fs.readFile(resolvedPath, 'utf-8');
84
+ const content = await this.readSessionFile(resolvedPath);
74
85
  const session = JSON.parse(content);
75
86
  await this.importSession(session);
76
87
  logger.info(`Session loaded from ${resolvedPath}`, {
@@ -93,7 +104,7 @@ export class DebuggerSessionManager {
93
104
  await this.debuggerManager.clearAllBreakpoints();
94
105
  let successCount = 0;
95
106
  let failCount = 0;
96
- for (const bp of session.breakpoints) {
107
+ await this.processInBatches(session.breakpoints, this.SESSION_IMPORT_BATCH_SIZE, async (bp) => {
97
108
  try {
98
109
  if (bp.location.url) {
99
110
  await this.debuggerManager.setBreakpointByUrl({
@@ -122,7 +133,7 @@ export class DebuggerSessionManager {
122
133
  logger.error('Failed to restore breakpoint:', error, bp);
123
134
  failCount++;
124
135
  }
125
- }
136
+ });
126
137
  if (session.pauseOnExceptions) {
127
138
  await this.debuggerManager.setPauseOnExceptions(session.pauseOnExceptions);
128
139
  }
@@ -143,23 +154,22 @@ export class DebuggerSessionManager {
143
154
  }
144
155
  const files = await fs.readdir(sessionsDir);
145
156
  const sessions = [];
146
- for (const file of files) {
147
- if (file.endsWith('.json')) {
148
- const filePath = path.join(sessionsDir, file);
149
- try {
150
- const content = await fs.readFile(filePath, 'utf-8');
151
- const session = JSON.parse(content);
152
- sessions.push({
153
- path: filePath,
154
- timestamp: session.timestamp,
155
- metadata: session.metadata,
156
- });
157
- }
158
- catch (error) {
159
- logger.warn(`Failed to read session file ${file}:`, error);
160
- }
157
+ const sessionFiles = files.filter((file) => file.endsWith('.json'));
158
+ await this.processInBatches(sessionFiles, this.SESSION_FILE_READ_BATCH_SIZE, async (file) => {
159
+ const filePath = path.join(sessionsDir, file);
160
+ try {
161
+ const content = await this.readSessionFile(filePath);
162
+ const session = JSON.parse(content);
163
+ sessions.push({
164
+ path: filePath,
165
+ timestamp: session.timestamp,
166
+ metadata: session.metadata,
167
+ });
161
168
  }
162
- }
169
+ catch (error) {
170
+ logger.warn(`Failed to read session file ${file}:`, error);
171
+ }
172
+ });
163
173
  sessions.sort((a, b) => b.timestamp - a.timestamp);
164
174
  return sessions;
165
175
  }
@@ -12,6 +12,9 @@ export interface ScriptInfo {
12
12
  }
13
13
  export declare class ScriptManager {
14
14
  private collector;
15
+ private static readonly SOURCE_LOAD_BATCH_SIZE;
16
+ private static readonly SEARCH_LINE_YIELD_INTERVAL;
17
+ private static readonly SEARCH_SCRIPT_YIELD_INTERVAL;
15
18
  private cdpSession;
16
19
  private scripts;
17
20
  private scriptsByUrl;
@@ -23,6 +26,7 @@ export declare class ScriptManager {
23
26
  constructor(collector: CodeCollector);
24
27
  init(): Promise<void>;
25
28
  private doInit;
29
+ private loadScriptSourceInternal;
26
30
  enable(): Promise<void>;
27
31
  getAllScripts(includeSource?: boolean, maxScripts?: number): Promise<ScriptInfo[]>;
28
32
  getScriptSource(scriptId?: string, url?: string): Promise<ScriptInfo | null>;
@@ -1,7 +1,11 @@
1
+ import { setImmediate as waitForImmediate } from 'node:timers/promises';
1
2
  import { logger } from '../../utils/logger.js';
2
3
  import { extractFunctionTreeCore, } from '../debugger/ScriptManager.impl.extract-function-tree.js';
3
4
  export class ScriptManager {
4
5
  collector;
6
+ static SOURCE_LOAD_BATCH_SIZE = 8;
7
+ static SEARCH_LINE_YIELD_INTERVAL = 250;
8
+ static SEARCH_SCRIPT_YIELD_INTERVAL = 10;
5
9
  cdpSession = null;
6
10
  scripts = new Map();
7
11
  scriptsByUrl = new Map();
@@ -54,6 +58,25 @@ export class ScriptManager {
54
58
  this.initialized = true;
55
59
  logger.info('ScriptManager initialized');
56
60
  }
61
+ async loadScriptSourceInternal(script) {
62
+ if (script.source) {
63
+ return true;
64
+ }
65
+ try {
66
+ const { scriptSource } = await this.cdpSession.send('Debugger.getScriptSource', {
67
+ scriptId: script.scriptId,
68
+ });
69
+ script.source = scriptSource;
70
+ script.sourceLength = scriptSource.length;
71
+ this.buildKeywordIndex(script.scriptId, script.url, scriptSource);
72
+ this.chunkScript(script.scriptId, scriptSource);
73
+ return true;
74
+ }
75
+ catch (error) {
76
+ logger.warn(`Failed to get source for script ${script.scriptId}:`, error);
77
+ return false;
78
+ }
79
+ }
57
80
  async enable() {
58
81
  return this.init();
59
82
  }
@@ -70,23 +93,27 @@ export class ScriptManager {
70
93
  logger.warn(`Loading source code for ${limitedScripts.length} scripts. This may use significant memory.`);
71
94
  let loadedCount = 0;
72
95
  let failedCount = 0;
73
- for (const script of limitedScripts) {
74
- if (!script.source) {
75
- try {
76
- const { scriptSource } = await this.cdpSession.send('Debugger.getScriptSource', {
77
- scriptId: script.scriptId,
78
- });
79
- script.source = scriptSource;
96
+ const missingScripts = limitedScripts.filter((script) => !script.source);
97
+ for (let batchStart = 0; batchStart < missingScripts.length; batchStart += ScriptManager.SOURCE_LOAD_BATCH_SIZE) {
98
+ const batch = missingScripts.slice(batchStart, batchStart + ScriptManager.SOURCE_LOAD_BATCH_SIZE);
99
+ const settled = await Promise.allSettled(batch.map(async (script) => {
100
+ const loaded = await this.loadScriptSourceInternal(script);
101
+ if (loaded) {
80
102
  loadedCount++;
81
103
  if (loadedCount % 10 === 0) {
82
104
  logger.debug(`Loaded ${loadedCount}/${limitedScripts.length} scripts...`);
83
105
  }
84
106
  }
85
- catch (error) {
86
- logger.warn(`Failed to get source for script ${script.scriptId}:`, error);
107
+ else {
108
+ failedCount++;
109
+ }
110
+ }));
111
+ for (const result of settled) {
112
+ if (result.status === 'rejected') {
87
113
  failedCount++;
88
114
  }
89
115
  }
116
+ await waitForImmediate();
90
117
  }
91
118
  logger.info(`getAllScripts: ${limitedScripts.length} scripts (loaded: ${loadedCount}, failed: ${failedCount})`);
92
119
  }
@@ -121,17 +148,9 @@ export class ScriptManager {
121
148
  return null;
122
149
  }
123
150
  if (!targetScript.source) {
124
- try {
125
- const { scriptSource } = await this.cdpSession.send('Debugger.getScriptSource', {
126
- scriptId: targetScript.scriptId,
127
- });
128
- targetScript.source = scriptSource;
129
- targetScript.sourceLength = scriptSource.length;
130
- this.buildKeywordIndex(targetScript.scriptId, targetScript.url, scriptSource);
131
- this.chunkScript(targetScript.scriptId, scriptSource);
132
- }
133
- catch (error) {
134
- logger.error(`Failed to get script source for ${targetScript.scriptId}:`, error);
151
+ const loaded = await this.loadScriptSourceInternal(targetScript);
152
+ if (!loaded) {
153
+ logger.error(`Failed to get script source for ${targetScript.scriptId}`);
135
154
  return null;
136
155
  }
137
156
  }
@@ -166,7 +185,7 @@ export class ScriptManager {
166
185
  : new RegExp(keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), caseSensitive ? 'g' : 'gi');
167
186
  const matches = [];
168
187
  const scripts = await this.getAllScripts(true, 500);
169
- for (const script of scripts) {
188
+ for (const [scriptIndex, script] of scripts.entries()) {
170
189
  if (!script.source)
171
190
  continue;
172
191
  if (matches.length >= maxMatches)
@@ -193,6 +212,12 @@ export class ScriptManager {
193
212
  context,
194
213
  });
195
214
  }
215
+ if ((i + 1) % ScriptManager.SEARCH_LINE_YIELD_INTERVAL === 0) {
216
+ await waitForImmediate();
217
+ }
218
+ }
219
+ if ((scriptIndex + 1) % ScriptManager.SEARCH_SCRIPT_YIELD_INTERVAL === 0) {
220
+ await waitForImmediate();
196
221
  }
197
222
  }
198
223
  logger.info(`searchInScripts: "${keyword}" - found ${matches.length} matches`);
@@ -12,8 +12,8 @@ export class EnvironmentEmulator {
12
12
  llm;
13
13
  constructor(llm) {
14
14
  this.llm = llm;
15
- if (llm) {
16
- logger.info('LLM service unavailable, skipping AI environment analysis');
15
+ if (!llm) {
16
+ logger.debug('LLM service unavailable, skipping AI environment analysis');
17
17
  }
18
18
  }
19
19
  async analyze(options) {