@silbercue/chrome 0.2.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 (129) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +229 -0
  3. package/build/cache/a11y-tree.d.ts +252 -0
  4. package/build/cache/a11y-tree.js +1956 -0
  5. package/build/cache/index.d.ts +8 -0
  6. package/build/cache/index.js +4 -0
  7. package/build/cache/selector-cache.d.ts +47 -0
  8. package/build/cache/selector-cache.js +119 -0
  9. package/build/cache/session-defaults.d.ts +27 -0
  10. package/build/cache/session-defaults.js +130 -0
  11. package/build/cache/tab-state-cache.d.ts +39 -0
  12. package/build/cache/tab-state-cache.js +171 -0
  13. package/build/cdp/cdp-client.d.ts +25 -0
  14. package/build/cdp/cdp-client.js +146 -0
  15. package/build/cdp/chrome-launcher.d.ts +85 -0
  16. package/build/cdp/chrome-launcher.js +502 -0
  17. package/build/cdp/console-collector.d.ts +53 -0
  18. package/build/cdp/console-collector.js +147 -0
  19. package/build/cdp/debug.d.ts +1 -0
  20. package/build/cdp/debug.js +6 -0
  21. package/build/cdp/dialog-handler.d.ts +54 -0
  22. package/build/cdp/dialog-handler.js +129 -0
  23. package/build/cdp/dom-watcher.d.ts +45 -0
  24. package/build/cdp/dom-watcher.js +195 -0
  25. package/build/cdp/emulation.d.ts +12 -0
  26. package/build/cdp/emulation.js +17 -0
  27. package/build/cdp/index.d.ts +11 -0
  28. package/build/cdp/index.js +6 -0
  29. package/build/cdp/network-collector.d.ts +77 -0
  30. package/build/cdp/network-collector.js +257 -0
  31. package/build/cdp/protocol.d.ts +20 -0
  32. package/build/cdp/protocol.js +1 -0
  33. package/build/cdp/session-manager.d.ts +62 -0
  34. package/build/cdp/session-manager.js +205 -0
  35. package/build/cdp/settle.d.ts +16 -0
  36. package/build/cdp/settle.js +71 -0
  37. package/build/cli/license-commands.d.ts +19 -0
  38. package/build/cli/license-commands.js +199 -0
  39. package/build/cli/top-level-commands.d.ts +49 -0
  40. package/build/cli/top-level-commands.js +222 -0
  41. package/build/hooks/index.d.ts +2 -0
  42. package/build/hooks/index.js +1 -0
  43. package/build/hooks/pro-hooks.d.ts +126 -0
  44. package/build/hooks/pro-hooks.js +17 -0
  45. package/build/index.d.ts +4 -0
  46. package/build/index.js +86 -0
  47. package/build/license/free-tier-config.d.ts +14 -0
  48. package/build/license/free-tier-config.js +18 -0
  49. package/build/license/index.d.ts +4 -0
  50. package/build/license/index.js +2 -0
  51. package/build/license/license-status.d.ts +15 -0
  52. package/build/license/license-status.js +9 -0
  53. package/build/overlay/session-overlay.d.ts +22 -0
  54. package/build/overlay/session-overlay.js +372 -0
  55. package/build/plan/index.d.ts +7 -0
  56. package/build/plan/index.js +4 -0
  57. package/build/plan/plan-conditions.d.ts +12 -0
  58. package/build/plan/plan-conditions.js +242 -0
  59. package/build/plan/plan-executor.d.ts +49 -0
  60. package/build/plan/plan-executor.js +259 -0
  61. package/build/plan/plan-state-store.d.ts +24 -0
  62. package/build/plan/plan-state-store.js +43 -0
  63. package/build/plan/plan-variables.d.ts +16 -0
  64. package/build/plan/plan-variables.js +71 -0
  65. package/build/registry.d.ts +124 -0
  66. package/build/registry.js +884 -0
  67. package/build/server.d.ts +1 -0
  68. package/build/server.js +245 -0
  69. package/build/tools/click.d.ts +34 -0
  70. package/build/tools/click.js +293 -0
  71. package/build/tools/configure-session.d.ts +15 -0
  72. package/build/tools/configure-session.js +45 -0
  73. package/build/tools/console-logs.d.ts +18 -0
  74. package/build/tools/console-logs.js +44 -0
  75. package/build/tools/dom-snapshot.d.ts +13 -0
  76. package/build/tools/dom-snapshot.js +259 -0
  77. package/build/tools/element-utils.d.ts +23 -0
  78. package/build/tools/element-utils.js +133 -0
  79. package/build/tools/error-utils.d.ts +8 -0
  80. package/build/tools/error-utils.js +27 -0
  81. package/build/tools/evaluate.d.ts +34 -0
  82. package/build/tools/evaluate.js +217 -0
  83. package/build/tools/file-upload.d.ts +20 -0
  84. package/build/tools/file-upload.js +174 -0
  85. package/build/tools/fill-form.d.ts +39 -0
  86. package/build/tools/fill-form.js +256 -0
  87. package/build/tools/handle-dialog.d.ts +15 -0
  88. package/build/tools/handle-dialog.js +48 -0
  89. package/build/tools/index.d.ts +35 -0
  90. package/build/tools/index.js +18 -0
  91. package/build/tools/navigate.d.ts +18 -0
  92. package/build/tools/navigate.js +111 -0
  93. package/build/tools/network-monitor.d.ts +18 -0
  94. package/build/tools/network-monitor.js +66 -0
  95. package/build/tools/observe.d.ts +44 -0
  96. package/build/tools/observe.js +339 -0
  97. package/build/tools/press-key.d.ts +33 -0
  98. package/build/tools/press-key.js +155 -0
  99. package/build/tools/read-page.d.ts +22 -0
  100. package/build/tools/read-page.js +100 -0
  101. package/build/tools/run-plan.d.ts +205 -0
  102. package/build/tools/run-plan.js +215 -0
  103. package/build/tools/screenshot.d.ts +16 -0
  104. package/build/tools/screenshot.js +283 -0
  105. package/build/tools/scroll.d.ts +28 -0
  106. package/build/tools/scroll.js +143 -0
  107. package/build/tools/switch-tab.d.ts +26 -0
  108. package/build/tools/switch-tab.js +355 -0
  109. package/build/tools/tab-status.d.ts +7 -0
  110. package/build/tools/tab-status.js +50 -0
  111. package/build/tools/type.d.ts +31 -0
  112. package/build/tools/type.js +247 -0
  113. package/build/tools/virtual-desk.d.ts +7 -0
  114. package/build/tools/virtual-desk.js +108 -0
  115. package/build/tools/visual-constants.d.ts +3 -0
  116. package/build/tools/visual-constants.js +10 -0
  117. package/build/tools/wait-for.d.ts +26 -0
  118. package/build/tools/wait-for.js +323 -0
  119. package/build/transport/index.d.ts +3 -0
  120. package/build/transport/index.js +2 -0
  121. package/build/transport/pipe-transport.d.ts +18 -0
  122. package/build/transport/pipe-transport.js +63 -0
  123. package/build/transport/transport.d.ts +8 -0
  124. package/build/transport/transport.js +1 -0
  125. package/build/transport/websocket-transport.d.ts +22 -0
  126. package/build/transport/websocket-transport.js +200 -0
  127. package/build/types.d.ts +21 -0
  128. package/build/types.js +1 -0
  129. package/package.json +62 -0
@@ -0,0 +1,8 @@
1
+ export { A11yTreeProcessor, a11yTree, RefNotFoundError } from "./a11y-tree.js";
2
+ export type { TreeOptions, TreeResult, AXNode } from "./a11y-tree.js";
3
+ export { TabStateCache } from "./tab-state-cache.js";
4
+ export type { TabState, TabStateCacheOptions } from "./tab-state-cache.js";
5
+ export { SessionDefaults } from "./session-defaults.js";
6
+ export type { SessionDefaultsOptions, PromoteSuggestion } from "./session-defaults.js";
7
+ export { SelectorCache, selectorCache } from "./selector-cache.js";
8
+ export type { SelectorCacheEntry, SelectorCacheOptions } from "./selector-cache.js";
@@ -0,0 +1,4 @@
1
+ export { A11yTreeProcessor, a11yTree, RefNotFoundError } from "./a11y-tree.js";
2
+ export { TabStateCache } from "./tab-state-cache.js";
3
+ export { SessionDefaults } from "./session-defaults.js";
4
+ export { SelectorCache, selectorCache } from "./selector-cache.js";
@@ -0,0 +1,47 @@
1
+ export interface SelectorCacheEntry {
2
+ backendNodeId: number;
3
+ fingerprint: string;
4
+ sessionId: string;
5
+ cachedAt: number;
6
+ }
7
+ export interface SelectorCacheOptions {
8
+ maxEntries?: number;
9
+ ttlMs?: number;
10
+ }
11
+ export declare class SelectorCache {
12
+ private _cache;
13
+ private _currentFingerprint;
14
+ private _maxEntries;
15
+ private _ttlMs;
16
+ constructor(options?: SelectorCacheOptions);
17
+ /**
18
+ * Compute a DOM fingerprint from URL and node count.
19
+ * Lightweight — no DOMSnapshot, no A11y-Tree.
20
+ */
21
+ computeFingerprint(url: string, nodeCount: number): string;
22
+ /** Set the current DOM fingerprint (called after each ref resolution or tree refresh) */
23
+ updateFingerprint(fingerprint: string): void;
24
+ /**
25
+ * Lookup: Check if a cached selector for this ref exists.
26
+ * Returns entry if cache hit AND fingerprint matches.
27
+ * Returns undefined on miss or fingerprint mismatch.
28
+ */
29
+ get(ref: string): SelectorCacheEntry | undefined;
30
+ /**
31
+ * Store a resolved ref in the cache.
32
+ * Called after successful ref resolution.
33
+ * When url and nodeCount are provided, computes an on-the-fly fingerprint
34
+ * if none is active yet (fixes cold-cache after navigation — H1).
35
+ */
36
+ set(ref: string, backendNodeId: number, sessionId: string, url?: string, nodeCount?: number): void;
37
+ /** Invalidate the entire cache (e.g. on navigation, reconnect) */
38
+ invalidate(): void;
39
+ /** Remove only entries that don't match the current fingerprint */
40
+ pruneStale(): void;
41
+ /** Cache statistics for debugging */
42
+ getStats(): {
43
+ size: number;
44
+ fingerprint: string;
45
+ };
46
+ }
47
+ export declare const selectorCache: SelectorCache;
@@ -0,0 +1,119 @@
1
+ import { createHash } from "node:crypto";
2
+ import { debug } from "../cdp/debug.js";
3
+ // --- SelectorCache ---
4
+ export class SelectorCache {
5
+ _cache = new Map();
6
+ _currentFingerprint = "";
7
+ _maxEntries;
8
+ _ttlMs;
9
+ constructor(options) {
10
+ this._maxEntries = options?.maxEntries ?? 200;
11
+ this._ttlMs = options?.ttlMs ?? 300_000;
12
+ }
13
+ /**
14
+ * Compute a DOM fingerprint from URL and node count.
15
+ * Lightweight — no DOMSnapshot, no A11y-Tree.
16
+ */
17
+ computeFingerprint(url, nodeCount) {
18
+ // Strip hash/fragment from URL for normalization
19
+ const normalizedUrl = url.split("#")[0];
20
+ return createHash("sha256")
21
+ .update(normalizedUrl + "|" + nodeCount)
22
+ .digest("hex")
23
+ .substring(0, 16);
24
+ }
25
+ /** Set the current DOM fingerprint (called after each ref resolution or tree refresh) */
26
+ updateFingerprint(fingerprint) {
27
+ if (fingerprint !== this._currentFingerprint) {
28
+ this._currentFingerprint = fingerprint;
29
+ debug("SelectorCache: fingerprint updated: %s", fingerprint);
30
+ }
31
+ }
32
+ /**
33
+ * Lookup: Check if a cached selector for this ref exists.
34
+ * Returns entry if cache hit AND fingerprint matches.
35
+ * Returns undefined on miss or fingerprint mismatch.
36
+ */
37
+ get(ref) {
38
+ const entry = this._cache.get(ref);
39
+ if (!entry) {
40
+ return undefined;
41
+ }
42
+ // Fingerprint mismatch — DOM has changed
43
+ if (entry.fingerprint !== this._currentFingerprint) {
44
+ this._cache.delete(ref);
45
+ debug("SelectorCache: fingerprint mismatch for %s, invalidating entry", ref);
46
+ return undefined;
47
+ }
48
+ // TTL expired
49
+ if (Date.now() - entry.cachedAt >= this._ttlMs) {
50
+ this._cache.delete(ref);
51
+ debug("SelectorCache: TTL expired for %s", ref);
52
+ return undefined;
53
+ }
54
+ return entry;
55
+ }
56
+ /**
57
+ * Store a resolved ref in the cache.
58
+ * Called after successful ref resolution.
59
+ * When url and nodeCount are provided, computes an on-the-fly fingerprint
60
+ * if none is active yet (fixes cold-cache after navigation — H1).
61
+ */
62
+ set(ref, backendNodeId, sessionId, url, nodeCount) {
63
+ // Compute on-the-fly fingerprint if none active but URL is known (H1 fix)
64
+ if (!this._currentFingerprint && url !== undefined && nodeCount !== undefined) {
65
+ const fp = this.computeFingerprint(url, nodeCount);
66
+ this.updateFingerprint(fp);
67
+ debug("SelectorCache: on-the-fly fingerprint from set(): %s", fp);
68
+ }
69
+ // Still no fingerprint — can't cache
70
+ if (!this._currentFingerprint)
71
+ return;
72
+ // Evict oldest entry if at capacity
73
+ if (this._cache.size >= this._maxEntries && !this._cache.has(ref)) {
74
+ let oldestKey = null;
75
+ let oldestTime = Infinity;
76
+ for (const [key, entry] of this._cache) {
77
+ if (entry.cachedAt < oldestTime) {
78
+ oldestTime = entry.cachedAt;
79
+ oldestKey = key;
80
+ }
81
+ }
82
+ if (oldestKey !== null) {
83
+ this._cache.delete(oldestKey);
84
+ }
85
+ }
86
+ this._cache.set(ref, {
87
+ backendNodeId,
88
+ fingerprint: this._currentFingerprint,
89
+ sessionId,
90
+ cachedAt: Date.now(),
91
+ });
92
+ }
93
+ /** Invalidate the entire cache (e.g. on navigation, reconnect) */
94
+ invalidate() {
95
+ const count = this._cache.size;
96
+ this._cache.clear();
97
+ this._currentFingerprint = "";
98
+ if (count > 0) {
99
+ debug("SelectorCache: invalidated (%d entries cleared)", count);
100
+ }
101
+ }
102
+ /** Remove only entries that don't match the current fingerprint */
103
+ pruneStale() {
104
+ for (const [key, entry] of this._cache) {
105
+ if (entry.fingerprint !== this._currentFingerprint) {
106
+ this._cache.delete(key);
107
+ }
108
+ }
109
+ }
110
+ /** Cache statistics for debugging */
111
+ getStats() {
112
+ return {
113
+ size: this._cache.size,
114
+ fingerprint: this._currentFingerprint,
115
+ };
116
+ }
117
+ }
118
+ // --- Singleton ---
119
+ export const selectorCache = new SelectorCache();
@@ -0,0 +1,27 @@
1
+ export interface SessionDefaultsOptions {
2
+ promoteThreshold?: number;
3
+ slidingWindowSize?: number;
4
+ }
5
+ export interface PromoteSuggestion {
6
+ param: string;
7
+ value: unknown;
8
+ count: number;
9
+ tool: string;
10
+ }
11
+ export declare class SessionDefaults {
12
+ private _defaults;
13
+ private _callHistory;
14
+ private _promoteThreshold;
15
+ private _slidingWindowSize;
16
+ private _pendingSuggestions;
17
+ constructor(options?: SessionDefaultsOptions);
18
+ setDefault(param: string, value: unknown): void;
19
+ getDefault(param: string): unknown | undefined;
20
+ getAllDefaults(): Record<string, unknown>;
21
+ clearAll(): void;
22
+ resolveParams(tool: string, params: Record<string, unknown>): Record<string, unknown>;
23
+ trackCall(tool: string, params: Record<string, unknown>): void;
24
+ getSuggestions(): PromoteSuggestion[];
25
+ applyAllSuggestions(): Record<string, unknown>;
26
+ private _analyzeForPromotions;
27
+ }
@@ -0,0 +1,130 @@
1
+ export class SessionDefaults {
2
+ _defaults;
3
+ _callHistory;
4
+ _promoteThreshold;
5
+ _slidingWindowSize;
6
+ _pendingSuggestions;
7
+ constructor(options) {
8
+ this._defaults = new Map();
9
+ this._callHistory = [];
10
+ this._promoteThreshold = options?.promoteThreshold ?? 3;
11
+ this._slidingWindowSize = options?.slidingWindowSize ?? 10;
12
+ this._pendingSuggestions = [];
13
+ }
14
+ // --- Default-Verwaltung ---
15
+ setDefault(param, value) {
16
+ if (value === null) {
17
+ this._defaults.delete(param);
18
+ }
19
+ else {
20
+ this._defaults.set(param, value);
21
+ }
22
+ }
23
+ getDefault(param) {
24
+ return this._defaults.get(param);
25
+ }
26
+ getAllDefaults() {
27
+ const result = {};
28
+ for (const [key, value] of this._defaults) {
29
+ result[key] = value;
30
+ }
31
+ return result;
32
+ }
33
+ clearAll() {
34
+ this._defaults.clear();
35
+ this._callHistory = [];
36
+ this._pendingSuggestions = [];
37
+ }
38
+ // --- Default-Resolution (Kernmethode) ---
39
+ resolveParams(tool, params) {
40
+ const resolved = { ...params };
41
+ for (const [key, defaultValue] of this._defaults) {
42
+ if (resolved[key] === undefined) {
43
+ resolved[key] = defaultValue;
44
+ }
45
+ }
46
+ return resolved;
47
+ }
48
+ // --- Auto-Promote-Tracking ---
49
+ trackCall(tool, params) {
50
+ this._callHistory.push({ tool, params });
51
+ // Front-trunc: keep only the last _slidingWindowSize entries
52
+ if (this._callHistory.length > this._slidingWindowSize) {
53
+ this._callHistory = this._callHistory.slice(-this._slidingWindowSize);
54
+ }
55
+ this._analyzeForPromotions();
56
+ }
57
+ getSuggestions() {
58
+ return [...this._pendingSuggestions];
59
+ }
60
+ applyAllSuggestions() {
61
+ const applied = {};
62
+ for (const suggestion of this._pendingSuggestions) {
63
+ this.setDefault(suggestion.param, suggestion.value);
64
+ applied[suggestion.param] = suggestion.value;
65
+ }
66
+ this._pendingSuggestions = [];
67
+ return applied;
68
+ }
69
+ // --- Private ---
70
+ _analyzeForPromotions() {
71
+ // Collect all param names from the call history
72
+ const paramNames = new Set();
73
+ for (const call of this._callHistory) {
74
+ for (const key of Object.keys(call.params)) {
75
+ paramNames.add(key);
76
+ }
77
+ }
78
+ const newSuggestions = [];
79
+ for (const param of paramNames) {
80
+ // Skip if already a default
81
+ if (this._defaults.has(param))
82
+ continue;
83
+ // Count consecutive identical values from the END of the history
84
+ let count = 0;
85
+ let lastValue = undefined;
86
+ let lastTool = "";
87
+ for (let i = this._callHistory.length - 1; i >= 0; i--) {
88
+ const call = this._callHistory[i];
89
+ const value = call.params[param];
90
+ // Skip calls that don't have this param
91
+ if (value === undefined)
92
+ break;
93
+ // Only compare primitives (string, number, boolean)
94
+ if (typeof value === "object" || typeof value === "function" || typeof value === "symbol")
95
+ break;
96
+ if (count === 0) {
97
+ lastValue = value;
98
+ lastTool = call.tool;
99
+ count = 1;
100
+ }
101
+ else if (value === lastValue) {
102
+ count++;
103
+ }
104
+ else {
105
+ break;
106
+ }
107
+ }
108
+ if (count >= this._promoteThreshold) {
109
+ newSuggestions.push({
110
+ param,
111
+ value: lastValue,
112
+ count,
113
+ tool: lastTool,
114
+ });
115
+ }
116
+ }
117
+ // Merge new suggestions into pending (update count for existing param, add new ones)
118
+ for (const newSugg of newSuggestions) {
119
+ const existingIdx = this._pendingSuggestions.findIndex((s) => s.param === newSugg.param);
120
+ if (existingIdx >= 0) {
121
+ this._pendingSuggestions[existingIdx] = newSugg;
122
+ }
123
+ else {
124
+ this._pendingSuggestions.push(newSugg);
125
+ }
126
+ }
127
+ // Remove suggestions for params that dropped below threshold or changed value
128
+ this._pendingSuggestions = this._pendingSuggestions.filter((s) => newSuggestions.some((n) => n.param === s.param));
129
+ }
130
+ }
@@ -0,0 +1,39 @@
1
+ import type { CdpClient } from "../cdp/cdp-client.js";
2
+ export interface TabState {
3
+ url: string;
4
+ title: string;
5
+ domReady: boolean;
6
+ consoleErrors: string[];
7
+ loadingState: "loading" | "ready";
8
+ lastUpdated: number;
9
+ }
10
+ export interface TabStateCacheOptions {
11
+ ttlMs?: number;
12
+ maxConsoleErrors?: number;
13
+ }
14
+ export declare class TabStateCache {
15
+ private readonly _cache;
16
+ private readonly _pendingErrors;
17
+ private readonly _ttlMs;
18
+ private readonly _maxConsoleErrors;
19
+ private _activeTargetId;
20
+ private _listeners;
21
+ private _cdpClient;
22
+ constructor(options?: TabStateCacheOptions);
23
+ get activeTargetId(): string | null;
24
+ setActiveTarget(targetId: string): void;
25
+ get(targetId: string): TabState | null;
26
+ set(targetId: string, state: Partial<TabState>): void;
27
+ invalidate(targetId: string): void;
28
+ invalidateAll(): void;
29
+ addConsoleError(targetId: string, error: string): void;
30
+ has(targetId: string): boolean;
31
+ size(): number;
32
+ attachToClient(cdpClient: CdpClient, sessionId?: string): void;
33
+ detachFromClient(): void;
34
+ getOrFetch(cdpClient: CdpClient, targetId: string, sessionId?: string): Promise<{
35
+ state: TabState;
36
+ cacheHit: boolean;
37
+ }>;
38
+ private _fetchFromCdp;
39
+ }
@@ -0,0 +1,171 @@
1
+ export class TabStateCache {
2
+ _cache = new Map();
3
+ _pendingErrors = new Map();
4
+ _ttlMs;
5
+ _maxConsoleErrors;
6
+ _activeTargetId = null;
7
+ _listeners = [];
8
+ _cdpClient = null;
9
+ constructor(options) {
10
+ this._ttlMs = options?.ttlMs ?? 30_000;
11
+ this._maxConsoleErrors = options?.maxConsoleErrors ?? 10;
12
+ }
13
+ get activeTargetId() {
14
+ return this._activeTargetId;
15
+ }
16
+ setActiveTarget(targetId) {
17
+ this._activeTargetId = targetId;
18
+ }
19
+ get(targetId) {
20
+ const entry = this._cache.get(targetId);
21
+ if (!entry)
22
+ return null;
23
+ if (Date.now() - entry.lastUpdated > this._ttlMs)
24
+ return null;
25
+ return entry;
26
+ }
27
+ set(targetId, state) {
28
+ const existing = this._cache.get(targetId);
29
+ if (existing) {
30
+ this._cache.set(targetId, {
31
+ ...existing,
32
+ ...state,
33
+ lastUpdated: Date.now(),
34
+ });
35
+ }
36
+ else {
37
+ this._cache.set(targetId, {
38
+ url: "",
39
+ title: "",
40
+ domReady: false,
41
+ consoleErrors: [],
42
+ loadingState: "loading",
43
+ ...state,
44
+ lastUpdated: Date.now(),
45
+ });
46
+ }
47
+ }
48
+ invalidate(targetId) {
49
+ this._cache.delete(targetId);
50
+ }
51
+ invalidateAll() {
52
+ this._cache.clear();
53
+ }
54
+ addConsoleError(targetId, error) {
55
+ const entry = this._cache.get(targetId);
56
+ if (entry) {
57
+ entry.consoleErrors.push(error);
58
+ if (entry.consoleErrors.length > this._maxConsoleErrors) {
59
+ entry.consoleErrors = entry.consoleErrors.slice(entry.consoleErrors.length - this._maxConsoleErrors);
60
+ }
61
+ }
62
+ else {
63
+ // No full cache entry yet — buffer errors for later merge during CDP fetch
64
+ let pending = this._pendingErrors.get(targetId);
65
+ if (!pending) {
66
+ pending = [];
67
+ this._pendingErrors.set(targetId, pending);
68
+ }
69
+ pending.push(error);
70
+ if (pending.length > this._maxConsoleErrors) {
71
+ this._pendingErrors.set(targetId, pending.slice(pending.length - this._maxConsoleErrors));
72
+ }
73
+ }
74
+ }
75
+ has(targetId) {
76
+ return this.get(targetId) !== null;
77
+ }
78
+ size() {
79
+ return this._cache.size;
80
+ }
81
+ attachToClient(cdpClient, sessionId) {
82
+ this._cdpClient = cdpClient;
83
+ const onFrameNavigated = (params) => {
84
+ const p = params;
85
+ if (!p.frame.parentId && this._activeTargetId) {
86
+ const targetId = this._activeTargetId;
87
+ this.invalidate(targetId);
88
+ // H3: Auto-prefill cache after invalidation (fire-and-forget)
89
+ this._fetchFromCdp(cdpClient, targetId, sessionId)
90
+ .then((state) => this._cache.set(targetId, state))
91
+ .catch(() => {
92
+ /* prefill is best-effort */
93
+ });
94
+ }
95
+ };
96
+ const onNavigatedWithinDocument = () => {
97
+ if (this._activeTargetId) {
98
+ this.invalidate(this._activeTargetId);
99
+ }
100
+ };
101
+ const onDomContentEventFired = () => {
102
+ if (this._activeTargetId) {
103
+ const existing = this._cache.get(this._activeTargetId);
104
+ if (existing) {
105
+ existing.domReady = true;
106
+ }
107
+ }
108
+ };
109
+ const onExceptionThrown = (params) => {
110
+ const p = params;
111
+ const msg = p.exceptionDetails.exception?.description || p.exceptionDetails.text;
112
+ if (this._activeTargetId) {
113
+ this.addConsoleError(this._activeTargetId, msg);
114
+ }
115
+ };
116
+ cdpClient.on("Page.frameNavigated", onFrameNavigated, sessionId);
117
+ cdpClient.on("Page.navigatedWithinDocument", onNavigatedWithinDocument, sessionId);
118
+ cdpClient.on("Page.domContentEventFired", onDomContentEventFired, sessionId);
119
+ cdpClient.on("Runtime.exceptionThrown", onExceptionThrown, sessionId);
120
+ this._listeners = [
121
+ { method: "Page.frameNavigated", callback: onFrameNavigated },
122
+ { method: "Page.navigatedWithinDocument", callback: onNavigatedWithinDocument },
123
+ { method: "Page.domContentEventFired", callback: onDomContentEventFired },
124
+ { method: "Runtime.exceptionThrown", callback: onExceptionThrown },
125
+ ];
126
+ }
127
+ detachFromClient() {
128
+ if (this._cdpClient) {
129
+ for (const { method, callback } of this._listeners) {
130
+ this._cdpClient.off(method, callback);
131
+ }
132
+ }
133
+ this._listeners = [];
134
+ this._cdpClient = null;
135
+ }
136
+ async getOrFetch(cdpClient, targetId, sessionId) {
137
+ const cached = this.get(targetId);
138
+ if (cached) {
139
+ return { state: cached, cacheHit: true };
140
+ }
141
+ const state = await this._fetchFromCdp(cdpClient, targetId, sessionId);
142
+ this._cache.set(targetId, state);
143
+ return { state, cacheHit: false };
144
+ }
145
+ async _fetchFromCdp(cdpClient, targetId, sessionId) {
146
+ const [navHistory, readyState] = await Promise.all([
147
+ cdpClient.send("Page.getNavigationHistory", {}, sessionId),
148
+ cdpClient.send("Runtime.evaluate", { expression: "document.readyState", returnByValue: true }, sessionId),
149
+ ]);
150
+ const currentEntry = navHistory.entries[navHistory.currentIndex];
151
+ // Merge console errors: existing (stale) cache entry + pending (buffered) errors
152
+ const existingErrors = this._cache.get(targetId)?.consoleErrors ?? [];
153
+ const pendingErrors = this._pendingErrors.get(targetId) ?? [];
154
+ const mergedErrors = [...existingErrors, ...pendingErrors];
155
+ // Cap at maxConsoleErrors (keep most recent)
156
+ const consoleErrors = mergedErrors.length > this._maxConsoleErrors
157
+ ? mergedErrors.slice(mergedErrors.length - this._maxConsoleErrors)
158
+ : mergedErrors;
159
+ // H1: Consume pending errors — prevent memory leak
160
+ this._pendingErrors.delete(targetId);
161
+ return {
162
+ url: currentEntry.url,
163
+ title: currentEntry.title,
164
+ domReady: readyState.result.value === "interactive" ||
165
+ readyState.result.value === "complete",
166
+ consoleErrors,
167
+ loadingState: readyState.result.value === "complete" ? "ready" : "loading",
168
+ lastUpdated: Date.now(),
169
+ };
170
+ }
171
+ }
@@ -0,0 +1,25 @@
1
+ import type { CdpTransport } from "../transport/transport.js";
2
+ export interface CdpClientOptions {
3
+ timeoutMs?: number;
4
+ }
5
+ type EventCallback = (params: unknown, sessionId?: string) => void;
6
+ export declare class CdpClient {
7
+ private readonly transport;
8
+ private _nextId;
9
+ private readonly _pending;
10
+ private readonly _listeners;
11
+ private readonly _onceListeners;
12
+ private readonly _timeoutMs;
13
+ private _closed;
14
+ private _onCloseCallback;
15
+ constructor(transport: CdpTransport, options?: CdpClientOptions);
16
+ send<T = unknown>(method: string, params?: Record<string, unknown>, sessionId?: string): Promise<T>;
17
+ on(method: string, callback: EventCallback, sessionId?: string): void;
18
+ once(method: string, callback: EventCallback, sessionId?: string): void;
19
+ off(method: string, callback: EventCallback): void;
20
+ onClose(callback: () => void): void;
21
+ close(): Promise<void>;
22
+ private _dispatch;
23
+ private _rejectAll;
24
+ }
25
+ export {};