opencode-pair-autonomy 1.0.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 (128) hide show
  1. package/README.md +90 -0
  2. package/bin/opencode-pair-autonomy.js +20 -0
  3. package/dist/__tests__/comment-guard.test.d.ts +1 -0
  4. package/dist/__tests__/config.test.d.ts +1 -0
  5. package/dist/__tests__/learning.test.d.ts +1 -0
  6. package/dist/__tests__/plan-mode.test.d.ts +1 -0
  7. package/dist/agents.d.ts +2 -0
  8. package/dist/cli.d.ts +1 -0
  9. package/dist/cli.js +15351 -0
  10. package/dist/commands.d.ts +2 -0
  11. package/dist/config.d.ts +3 -0
  12. package/dist/hooks/comment-guard.d.ts +15 -0
  13. package/dist/hooks/file-edited.d.ts +7 -0
  14. package/dist/hooks/index.d.ts +46 -0
  15. package/dist/hooks/post-tool-use.d.ts +5 -0
  16. package/dist/hooks/pre-compact.d.ts +4 -0
  17. package/dist/hooks/pre-tool-use.d.ts +5 -0
  18. package/dist/hooks/prompt-refiner.d.ts +38 -0
  19. package/dist/hooks/runtime.d.ts +91 -0
  20. package/dist/hooks/sdk.d.ts +6 -0
  21. package/dist/hooks/session-end.d.ts +4 -0
  22. package/dist/hooks/session-start.d.ts +19 -0
  23. package/dist/hooks/stop.d.ts +5 -0
  24. package/dist/i18n/index.d.ts +15 -0
  25. package/dist/index.d.ts +3 -0
  26. package/dist/index.js +17823 -0
  27. package/dist/installer.d.ts +12 -0
  28. package/dist/learning/analyzer.d.ts +15 -0
  29. package/dist/learning/store.d.ts +4 -0
  30. package/dist/learning/types.d.ts +32 -0
  31. package/dist/mcp.d.ts +4 -0
  32. package/dist/project-facts.d.ts +8 -0
  33. package/dist/prompts/coordinator.d.ts +2 -0
  34. package/dist/prompts/shared.d.ts +5 -0
  35. package/dist/prompts/workers.d.ts +8 -0
  36. package/dist/types.d.ts +81 -0
  37. package/dist/utils.d.ts +6 -0
  38. package/examples/opencode-pair-autonomy.jsonc +35 -0
  39. package/examples/opencode.jsonc +17 -0
  40. package/package.json +103 -0
  41. package/vendor/mcp/pg-mcp/README.md +91 -0
  42. package/vendor/mcp/pg-mcp/config.example.json +26 -0
  43. package/vendor/mcp/pg-mcp/config.json +15 -0
  44. package/vendor/mcp/pg-mcp/package-lock.json +1288 -0
  45. package/vendor/mcp/pg-mcp/package.json +18 -0
  46. package/vendor/mcp/pg-mcp/src/config.js +71 -0
  47. package/vendor/mcp/pg-mcp/src/db.js +85 -0
  48. package/vendor/mcp/pg-mcp/src/index.js +203 -0
  49. package/vendor/mcp/pg-mcp/src/sqlGuard.js +75 -0
  50. package/vendor/mcp/pg-mcp/src/tools.js +89 -0
  51. package/vendor/mcp/ssh-mcp/README.md +46 -0
  52. package/vendor/mcp/ssh-mcp/config.example.json +23 -0
  53. package/vendor/mcp/ssh-mcp/config.json +6 -0
  54. package/vendor/mcp/ssh-mcp/package-lock.json +1142 -0
  55. package/vendor/mcp/ssh-mcp/package.json +18 -0
  56. package/vendor/mcp/ssh-mcp/src/config.js +140 -0
  57. package/vendor/mcp/ssh-mcp/src/index.js +130 -0
  58. package/vendor/mcp/ssh-mcp/src/ssh.js +163 -0
  59. package/vendor/mcp/sudo-mcp/README.md +51 -0
  60. package/vendor/mcp/sudo-mcp/config.example.json +28 -0
  61. package/vendor/mcp/sudo-mcp/config.json +28 -0
  62. package/vendor/mcp/sudo-mcp/package-lock.json +1145 -0
  63. package/vendor/mcp/sudo-mcp/package.json +18 -0
  64. package/vendor/mcp/sudo-mcp/src/config.js +57 -0
  65. package/vendor/mcp/sudo-mcp/src/index.js +267 -0
  66. package/vendor/mcp/sudo-mcp/src/runner.js +168 -0
  67. package/vendor/mcp/web-agent-mcp/package-lock.json +2886 -0
  68. package/vendor/mcp/web-agent-mcp/package.json +28 -0
  69. package/vendor/mcp/web-agent-mcp/src/adapters/cloakbrowser/adapter.ts +335 -0
  70. package/vendor/mcp/web-agent-mcp/src/adapters/cloakbrowser/auth-heuristics.ts +324 -0
  71. package/vendor/mcp/web-agent-mcp/src/adapters/cloakbrowser/launcher.ts +1340 -0
  72. package/vendor/mcp/web-agent-mcp/src/config/env.ts +107 -0
  73. package/vendor/mcp/web-agent-mcp/src/core/action-flow.ts +82 -0
  74. package/vendor/mcp/web-agent-mcp/src/core/artifact-store.ts +109 -0
  75. package/vendor/mcp/web-agent-mcp/src/core/errors.ts +108 -0
  76. package/vendor/mcp/web-agent-mcp/src/core/observation-flow.ts +38 -0
  77. package/vendor/mcp/web-agent-mcp/src/core/policy-engine.ts +113 -0
  78. package/vendor/mcp/web-agent-mcp/src/core/retry-policy.ts +42 -0
  79. package/vendor/mcp/web-agent-mcp/src/core/session-manager.ts +670 -0
  80. package/vendor/mcp/web-agent-mcp/src/core/session-restart-policy.ts +34 -0
  81. package/vendor/mcp/web-agent-mcp/src/core/task-history.ts +97 -0
  82. package/vendor/mcp/web-agent-mcp/src/index.ts +3 -0
  83. package/vendor/mcp/web-agent-mcp/src/schemas/act.ts +167 -0
  84. package/vendor/mcp/web-agent-mcp/src/schemas/common.ts +56 -0
  85. package/vendor/mcp/web-agent-mcp/src/schemas/observe.ts +214 -0
  86. package/vendor/mcp/web-agent-mcp/src/schemas/page.ts +21 -0
  87. package/vendor/mcp/web-agent-mcp/src/schemas/policy.ts +42 -0
  88. package/vendor/mcp/web-agent-mcp/src/schemas/runtime.ts +21 -0
  89. package/vendor/mcp/web-agent-mcp/src/schemas/session.ts +63 -0
  90. package/vendor/mcp/web-agent-mcp/src/server.ts +75 -0
  91. package/vendor/mcp/web-agent-mcp/src/tools/act/click.ts +68 -0
  92. package/vendor/mcp/web-agent-mcp/src/tools/act/drag.ts +57 -0
  93. package/vendor/mcp/web-agent-mcp/src/tools/act/enter-code.ts +78 -0
  94. package/vendor/mcp/web-agent-mcp/src/tools/act/fill.ts +65 -0
  95. package/vendor/mcp/web-agent-mcp/src/tools/act/pinch.ts +58 -0
  96. package/vendor/mcp/web-agent-mcp/src/tools/act/press.ts +67 -0
  97. package/vendor/mcp/web-agent-mcp/src/tools/act/shared.ts +73 -0
  98. package/vendor/mcp/web-agent-mcp/src/tools/act/swipe.ts +59 -0
  99. package/vendor/mcp/web-agent-mcp/src/tools/act/wait-for.ts +56 -0
  100. package/vendor/mcp/web-agent-mcp/src/tools/act/wheel.ts +59 -0
  101. package/vendor/mcp/web-agent-mcp/src/tools/observe/a11y.ts +60 -0
  102. package/vendor/mcp/web-agent-mcp/src/tools/observe/auth-state.ts +92 -0
  103. package/vendor/mcp/web-agent-mcp/src/tools/observe/boxes.ts +66 -0
  104. package/vendor/mcp/web-agent-mcp/src/tools/observe/console.ts +67 -0
  105. package/vendor/mcp/web-agent-mcp/src/tools/observe/dom.ts +60 -0
  106. package/vendor/mcp/web-agent-mcp/src/tools/observe/network.ts +67 -0
  107. package/vendor/mcp/web-agent-mcp/src/tools/observe/page-state.ts +93 -0
  108. package/vendor/mcp/web-agent-mcp/src/tools/observe/screenshot.ts +73 -0
  109. package/vendor/mcp/web-agent-mcp/src/tools/observe/text.ts +70 -0
  110. package/vendor/mcp/web-agent-mcp/src/tools/observe/wait-for-network.ts +70 -0
  111. package/vendor/mcp/web-agent-mcp/src/tools/page/navigate.ts +59 -0
  112. package/vendor/mcp/web-agent-mcp/src/tools/policy/recommend-observation.ts +40 -0
  113. package/vendor/mcp/web-agent-mcp/src/tools/register-tools.ts +55 -0
  114. package/vendor/mcp/web-agent-mcp/src/tools/runtime/evaluate-js.ts +83 -0
  115. package/vendor/mcp/web-agent-mcp/src/tools/session/close.ts +41 -0
  116. package/vendor/mcp/web-agent-mcp/src/tools/session/create.ts +86 -0
  117. package/vendor/mcp/web-agent-mcp/src/tools/session/restart.ts +72 -0
  118. package/vendor/mcp/web-agent-mcp/src/utils/fs.ts +28 -0
  119. package/vendor/mcp/web-agent-mcp/src/utils/ids.ts +9 -0
  120. package/vendor/mcp/web-agent-mcp/src/utils/time.ts +7 -0
  121. package/vendor/mcp/web-agent-mcp/tsconfig.json +22 -0
  122. package/vendor/skills/editorial-technical-ui/SKILL.md +84 -0
  123. package/vendor/skills/figma-console/SKILL.md +839 -0
  124. package/vendor/skills/go-fiber-postgres/SKILL.md +31 -0
  125. package/vendor/skills/opencode-plugin-dev/SKILL.md +31 -0
  126. package/vendor/skills/rust-media-desktop/SKILL.md +30 -0
  127. package/vendor/skills/vue-vite-ui/SKILL.md +31 -0
  128. package/vendor/skills/web-agent-browser/SKILL.md +140 -0
@@ -0,0 +1,670 @@
1
+ import type { WebAgentEnv } from "../config/env.js";
2
+ import { WebAgentError } from "./errors.js";
3
+ import { createId } from "../utils/ids.js";
4
+ import { nowIso } from "../utils/time.js";
5
+ import { shouldRecommendSessionRestart } from "./session-restart-policy.js";
6
+ import type {
7
+ AdapterConsoleEntry,
8
+ AdapterElementBox,
9
+ AdapterNetworkEntry,
10
+ AdapterProfileMode,
11
+ AdapterSessionHandle,
12
+ CloakBrowserAdapter,
13
+ WaitUntilState,
14
+ } from "../adapters/cloakbrowser/adapter.js";
15
+
16
+ export type ManagedPage = {
17
+ pageId: string;
18
+ createdAt: string;
19
+ url?: string;
20
+ title?: string;
21
+ lastObservationAt?: string;
22
+ lastActionAt?: string;
23
+ };
24
+
25
+ export type ManagedSession = {
26
+ sessionId: string;
27
+ contextId: string;
28
+ createdAt: string;
29
+ status: "active" | "closing" | "closed" | "error";
30
+ profileMode: AdapterProfileMode;
31
+ locale?: string;
32
+ timezoneId?: string;
33
+ userDataDir?: string;
34
+ profileDirectory?: string;
35
+ humanize: boolean;
36
+ launchArgs: string[];
37
+ viewport: {
38
+ width: number;
39
+ height: number;
40
+ };
41
+ consecutiveErrors: number;
42
+ lastErrorAt?: string;
43
+ lastRestartAt?: string;
44
+ pages: Map<string, ManagedPage>;
45
+ primaryPageId: string;
46
+ adapterSession: AdapterSessionHandle;
47
+ };
48
+
49
+ type SessionManagerDeps = {
50
+ env: WebAgentEnv;
51
+ adapter: CloakBrowserAdapter;
52
+ };
53
+
54
+ export class SessionManager {
55
+ private readonly sessions = new Map<string, ManagedSession>();
56
+ private readonly activeUserDataDirs = new Map<string, string>(); // userDataDir → sessionId
57
+
58
+ constructor(private readonly deps: SessionManagerDeps) {}
59
+
60
+ async createSession(input: {
61
+ profileMode: AdapterProfileMode;
62
+ locale?: string;
63
+ timezoneId?: string;
64
+ userDataDir?: string;
65
+ profileDirectory?: string;
66
+ humanize?: boolean;
67
+ launchArgs?: string[];
68
+ viewport?: { width: number; height: number };
69
+ }) {
70
+ const sessionId = createId("session");
71
+ const locale = input.locale ?? this.deps.env.defaultLocale;
72
+ const timezoneId = input.timezoneId ?? this.deps.env.defaultTimezoneId;
73
+ const userDataDir = input.userDataDir ?? this.deps.env.chromeUserDataDir;
74
+ const profileDirectory =
75
+ input.profileDirectory ?? this.deps.env.chromeProfileDirectory;
76
+ const humanize = input.humanize ?? this.deps.env.defaultHumanize;
77
+ const launchArgs = [
78
+ ...(input.launchArgs ?? this.deps.env.defaultLaunchArgs),
79
+ ...(profileDirectory ? [`--profile-directory=${profileDirectory}`] : []),
80
+ ];
81
+ const viewport = input.viewport ?? this.deps.env.defaultViewport;
82
+
83
+ if (input.profileMode === "persistent" && userDataDir) {
84
+ const conflictingSessionId = this.activeUserDataDirs.get(userDataDir);
85
+ if (conflictingSessionId) {
86
+ const conflicting = this.sessions.get(conflictingSessionId);
87
+ if (conflicting && conflicting.status === "active") {
88
+ throw new WebAgentError(
89
+ "STATE_PAGE_NOT_FOUND",
90
+ `Profile directory "${userDataDir}" is already in use by session ${conflictingSessionId}. Close that session first with session.close, or create a new session without a profile to use ephemeral mode.`,
91
+ { userDataDir, conflictingSessionId },
92
+ );
93
+ }
94
+ }
95
+ }
96
+
97
+ const adapterSession = await this.deps.adapter.createSession({
98
+ sessionId,
99
+ profileMode: input.profileMode,
100
+ locale,
101
+ timezoneId,
102
+ userDataDir,
103
+ profileDirectory,
104
+ humanize,
105
+ launchArgs,
106
+ viewport,
107
+ });
108
+
109
+ const page: ManagedPage = {
110
+ pageId: adapterSession.pageId,
111
+ createdAt: nowIso(),
112
+ };
113
+
114
+ const session: ManagedSession = {
115
+ sessionId,
116
+ contextId: adapterSession.contextId,
117
+ createdAt: nowIso(),
118
+ status: "active",
119
+ profileMode: input.profileMode,
120
+ locale,
121
+ timezoneId,
122
+ userDataDir,
123
+ profileDirectory,
124
+ humanize,
125
+ launchArgs,
126
+ viewport,
127
+ consecutiveErrors: 0,
128
+ primaryPageId: page.pageId,
129
+ pages: new Map([[page.pageId, page]]),
130
+ adapterSession,
131
+ };
132
+
133
+ this.sessions.set(sessionId, session);
134
+ if (input.profileMode === "persistent" && userDataDir) {
135
+ this.activeUserDataDirs.set(userDataDir, sessionId);
136
+ }
137
+ return session;
138
+ }
139
+
140
+ getSession(sessionId: string) {
141
+ const session = this.sessions.get(sessionId);
142
+ if (!session || session.status === "closed") {
143
+ throw new WebAgentError(
144
+ "INPUT_MISSING_SESSION",
145
+ `Session not found: ${sessionId}`,
146
+ { sessionId },
147
+ );
148
+ }
149
+ return session;
150
+ }
151
+
152
+ getPage(sessionId: string, pageId?: string) {
153
+ const session = this.getSession(sessionId);
154
+ const resolvedPageId = pageId ?? session.primaryPageId;
155
+ const page = session.pages.get(resolvedPageId);
156
+ if (!page) {
157
+ throw new WebAgentError(
158
+ "STATE_PAGE_NOT_FOUND",
159
+ `Page not found: ${resolvedPageId}`,
160
+ {
161
+ sessionId,
162
+ pageId: resolvedPageId,
163
+ },
164
+ );
165
+ }
166
+ return { session, page };
167
+ }
168
+
169
+ updatePage(sessionId: string, pageId: string, patch: Partial<ManagedPage>) {
170
+ const { session, page } = this.getPage(sessionId, pageId);
171
+ session.pages.set(pageId, { ...page, ...patch });
172
+ }
173
+
174
+ recordSuccess(sessionId: string) {
175
+ const session = this.getSession(sessionId);
176
+ session.consecutiveErrors = 0;
177
+ session.lastErrorAt = undefined;
178
+ this.sessions.set(sessionId, session);
179
+ }
180
+
181
+ recordFailure(sessionId: string) {
182
+ const session = this.getSession(sessionId);
183
+ session.consecutiveErrors += 1;
184
+ session.lastErrorAt = nowIso();
185
+ this.sessions.set(sessionId, session);
186
+ return session;
187
+ }
188
+
189
+ getSessionHealth(sessionId: string) {
190
+ const session = this.getSession(sessionId);
191
+ return {
192
+ consecutiveErrors: session.consecutiveErrors,
193
+ lastErrorAt: session.lastErrorAt,
194
+ lastRestartAt: session.lastRestartAt,
195
+ restartRecommended: shouldRecommendSessionRestart({
196
+ consecutiveErrors: session.consecutiveErrors,
197
+ maxConsecutiveErrors: this.deps.env.sessionMaxConsecutiveErrors,
198
+ cooldownMs: this.deps.env.sessionRestartCooldownMs,
199
+ lastRestartAt: session.lastRestartAt,
200
+ now: nowIso(),
201
+ browserError: false,
202
+ }).recommended,
203
+ };
204
+ }
205
+
206
+ async restartSession(sessionId: string) {
207
+ const session = this.getSession(sessionId);
208
+ const previous = {
209
+ profileMode: session.profileMode,
210
+ locale: session.locale,
211
+ timezoneId: session.timezoneId,
212
+ userDataDir: session.userDataDir,
213
+ profileDirectory: session.profileDirectory,
214
+ humanize: session.humanize,
215
+ launchArgs: session.launchArgs,
216
+ viewport: session.viewport,
217
+ };
218
+ await this.deps.adapter.closeSession(session.adapterSession);
219
+ if (previous.userDataDir) {
220
+ this.activeUserDataDirs.delete(previous.userDataDir);
221
+ }
222
+ const adapterSession = await this.deps.adapter.createSession({
223
+ sessionId,
224
+ profileMode: previous.profileMode,
225
+ locale: previous.locale ?? this.deps.env.defaultLocale,
226
+ timezoneId: previous.timezoneId,
227
+ humanize: previous.humanize,
228
+ launchArgs: previous.launchArgs,
229
+ viewport: previous.viewport,
230
+ });
231
+ const restarted: ManagedSession = {
232
+ ...session,
233
+ contextId: adapterSession.contextId,
234
+ status: "active",
235
+ consecutiveErrors: 0,
236
+ lastErrorAt: undefined,
237
+ lastRestartAt: nowIso(),
238
+ primaryPageId: adapterSession.pageId,
239
+ pages: new Map([
240
+ [
241
+ adapterSession.pageId,
242
+ { pageId: adapterSession.pageId, createdAt: nowIso() },
243
+ ],
244
+ ]),
245
+ adapterSession,
246
+ };
247
+ this.sessions.set(sessionId, restarted);
248
+ if (previous.profileMode === "persistent" && previous.userDataDir) {
249
+ this.activeUserDataDirs.set(previous.userDataDir, sessionId);
250
+ }
251
+ return restarted;
252
+ }
253
+
254
+ async navigate(
255
+ sessionId: string,
256
+ pageId: string | undefined,
257
+ url: string,
258
+ waitUntil: WaitUntilState,
259
+ ) {
260
+ const { session, page } = this.getPage(sessionId, pageId);
261
+ const result = await this.deps.adapter.navigate(
262
+ session.adapterSession,
263
+ url,
264
+ waitUntil,
265
+ );
266
+ this.updatePage(session.sessionId, page.pageId, {
267
+ url: result.finalUrl,
268
+ title: result.title,
269
+ lastActionAt: nowIso(),
270
+ });
271
+ return { session, page: this.getPage(sessionId, page.pageId).page, result };
272
+ }
273
+
274
+ async observeA11y(sessionId: string, pageId?: string) {
275
+ const { session, page } = this.getPage(sessionId, pageId);
276
+ const result = await this.deps.adapter.observeA11y(session.adapterSession);
277
+ this.updatePage(session.sessionId, page.pageId, {
278
+ url: result.url,
279
+ title: result.title,
280
+ lastObservationAt: nowIso(),
281
+ });
282
+ return { session, page: this.getPage(sessionId, page.pageId).page, result };
283
+ }
284
+
285
+ async observeDom(sessionId: string, pageId?: string) {
286
+ const { session, page } = this.getPage(sessionId, pageId);
287
+ const result = await this.deps.adapter.observeDom(session.adapterSession);
288
+ this.updatePage(session.sessionId, page.pageId, {
289
+ url: result.url,
290
+ title: result.title,
291
+ lastObservationAt: nowIso(),
292
+ });
293
+ return { session, page: this.getPage(sessionId, page.pageId).page, result };
294
+ }
295
+
296
+ async observeText(
297
+ sessionId: string,
298
+ pageId: string | undefined,
299
+ format: "text" | "markdown",
300
+ ) {
301
+ const { session, page } = this.getPage(sessionId, pageId);
302
+ const result = await this.deps.adapter.observeText(
303
+ session.adapterSession,
304
+ format,
305
+ );
306
+ this.updatePage(session.sessionId, page.pageId, {
307
+ url: result.url,
308
+ title: result.title,
309
+ lastObservationAt: nowIso(),
310
+ });
311
+ return { session, page: this.getPage(sessionId, page.pageId).page, result };
312
+ }
313
+
314
+ async observePageState(
315
+ sessionId: string,
316
+ pageId: string | undefined,
317
+ recentNetworkLimit: number,
318
+ ) {
319
+ const { session, page } = this.getPage(sessionId, pageId);
320
+ const result = await this.deps.adapter.inspectPageState(
321
+ session.adapterSession,
322
+ recentNetworkLimit,
323
+ );
324
+ this.updatePage(session.sessionId, page.pageId, {
325
+ url: result.url,
326
+ title: result.title,
327
+ lastObservationAt: nowIso(),
328
+ });
329
+ return { session, page: this.getPage(sessionId, page.pageId).page, result };
330
+ }
331
+
332
+ async observeAuthState(
333
+ sessionId: string,
334
+ pageId: string | undefined,
335
+ recentNetworkLimit: number,
336
+ ) {
337
+ const { session, page } = this.getPage(sessionId, pageId);
338
+ const result = await this.deps.adapter.inspectAuthState(
339
+ session.adapterSession,
340
+ recentNetworkLimit,
341
+ );
342
+ this.updatePage(session.sessionId, page.pageId, {
343
+ url: result.url,
344
+ title: result.title,
345
+ lastObservationAt: nowIso(),
346
+ });
347
+ return { session, page: this.getPage(sessionId, page.pageId).page, result };
348
+ }
349
+
350
+ async takeScreenshot(
351
+ sessionId: string,
352
+ pageId: string | undefined,
353
+ mode: "viewport" | "full" | "element",
354
+ format: "png" | "jpeg",
355
+ quality?: number,
356
+ selector?: string,
357
+ ) {
358
+ const { session, page } = this.getPage(sessionId, pageId);
359
+ const result = await this.deps.adapter.takeScreenshot(
360
+ session.adapterSession,
361
+ mode,
362
+ format,
363
+ quality,
364
+ selector,
365
+ );
366
+ this.updatePage(session.sessionId, page.pageId, {
367
+ url: result.url,
368
+ title: result.title,
369
+ lastObservationAt: nowIso(),
370
+ });
371
+ return { session, page: this.getPage(sessionId, page.pageId).page, result };
372
+ }
373
+
374
+ async observeBoxes(
375
+ sessionId: string,
376
+ pageId: string | undefined,
377
+ selectors: string[],
378
+ ) {
379
+ const { session, page } = this.getPage(sessionId, pageId);
380
+ const result = await this.deps.adapter.observeBoxes(
381
+ session.adapterSession,
382
+ selectors,
383
+ );
384
+ this.updatePage(session.sessionId, page.pageId, {
385
+ lastObservationAt: nowIso(),
386
+ });
387
+ return { session, page: this.getPage(sessionId, page.pageId).page, result };
388
+ }
389
+
390
+ async observeConsole(
391
+ sessionId: string,
392
+ pageId: string | undefined,
393
+ limit: number,
394
+ ) {
395
+ const { session, page } = this.getPage(sessionId, pageId);
396
+ const result = await this.deps.adapter.observeConsole(
397
+ session.adapterSession,
398
+ limit,
399
+ );
400
+ this.updatePage(session.sessionId, page.pageId, {
401
+ lastObservationAt: nowIso(),
402
+ });
403
+ return { session, page: this.getPage(sessionId, page.pageId).page, result };
404
+ }
405
+
406
+ async observeNetwork(
407
+ sessionId: string,
408
+ pageId: string | undefined,
409
+ limit: number,
410
+ ) {
411
+ const { session, page } = this.getPage(sessionId, pageId);
412
+ const result = await this.deps.adapter.observeNetwork(
413
+ session.adapterSession,
414
+ limit,
415
+ );
416
+ this.updatePage(session.sessionId, page.pageId, {
417
+ lastObservationAt: nowIso(),
418
+ });
419
+ return { session, page: this.getPage(sessionId, page.pageId).page, result };
420
+ }
421
+
422
+ async waitForNetwork(
423
+ sessionId: string,
424
+ pageId: string | undefined,
425
+ input: {
426
+ urlPattern: string;
427
+ useRegex: boolean;
428
+ status?: number;
429
+ outcome?: "response" | "failed";
430
+ timeoutMs: number;
431
+ pollIntervalMs: number;
432
+ },
433
+ ) {
434
+ const { session, page } = this.getPage(sessionId, pageId);
435
+ const result = await this.deps.adapter.waitForNetwork(
436
+ session.adapterSession,
437
+ input,
438
+ );
439
+ this.updatePage(session.sessionId, page.pageId, {
440
+ url: result.url,
441
+ title: result.title,
442
+ lastObservationAt: nowIso(),
443
+ });
444
+ return { session, page: this.getPage(sessionId, page.pageId).page, result };
445
+ }
446
+
447
+ async evaluateJs(
448
+ sessionId: string,
449
+ pageId: string | undefined,
450
+ input: { expression: string; awaitPromise: boolean },
451
+ ) {
452
+ const { session, page } = this.getPage(sessionId, pageId);
453
+ const result = await this.deps.adapter.evaluateJs(
454
+ session.adapterSession,
455
+ input,
456
+ );
457
+ this.updatePage(session.sessionId, page.pageId, {
458
+ url: result.url,
459
+ title: result.title,
460
+ lastActionAt: nowIso(),
461
+ lastObservationAt: nowIso(),
462
+ });
463
+ return { session, page: this.getPage(sessionId, page.pageId).page, result };
464
+ }
465
+
466
+ async click(
467
+ sessionId: string,
468
+ pageId: string | undefined,
469
+ input: {
470
+ selector: string;
471
+ frameSelector?: string;
472
+ button: "left" | "right" | "middle";
473
+ clickCount: number;
474
+ timeoutMs?: number;
475
+ },
476
+ ) {
477
+ const { session, page } = this.getPage(sessionId, pageId);
478
+ const result = await this.deps.adapter.click(session.adapterSession, input);
479
+ this.updatePage(session.sessionId, page.pageId, {
480
+ url: result.url,
481
+ title: result.title,
482
+ lastActionAt: nowIso(),
483
+ });
484
+ return { session, page: this.getPage(sessionId, page.pageId).page, result };
485
+ }
486
+
487
+ async fill(
488
+ sessionId: string,
489
+ pageId: string | undefined,
490
+ input: {
491
+ selector: string;
492
+ frameSelector?: string;
493
+ value: string;
494
+ clearFirst: boolean;
495
+ timeoutMs?: number;
496
+ },
497
+ ) {
498
+ const { session, page } = this.getPage(sessionId, pageId);
499
+ const result = await this.deps.adapter.fill(session.adapterSession, input);
500
+ this.updatePage(session.sessionId, page.pageId, {
501
+ url: result.url,
502
+ title: result.title,
503
+ lastActionAt: nowIso(),
504
+ });
505
+ return { session, page: this.getPage(sessionId, page.pageId).page, result };
506
+ }
507
+
508
+ async enterCode(
509
+ sessionId: string,
510
+ pageId: string | undefined,
511
+ input: {
512
+ code: string;
513
+ selector?: string;
514
+ frameSelector?: string;
515
+ submit: boolean;
516
+ timeoutMs?: number;
517
+ },
518
+ ) {
519
+ const { session, page } = this.getPage(sessionId, pageId);
520
+ const result = await this.deps.adapter.enterCode(
521
+ session.adapterSession,
522
+ input,
523
+ );
524
+ this.updatePage(session.sessionId, page.pageId, {
525
+ url: result.url,
526
+ title: result.title,
527
+ lastActionAt: nowIso(),
528
+ });
529
+ return { session, page: this.getPage(sessionId, page.pageId).page, result };
530
+ }
531
+
532
+ async press(
533
+ sessionId: string,
534
+ pageId: string | undefined,
535
+ input: {
536
+ key: string;
537
+ selector?: string;
538
+ frameSelector?: string;
539
+ timeoutMs?: number;
540
+ },
541
+ ) {
542
+ const { session, page } = this.getPage(sessionId, pageId);
543
+ const result = await this.deps.adapter.press(session.adapterSession, input);
544
+ this.updatePage(session.sessionId, page.pageId, {
545
+ url: result.url,
546
+ title: result.title,
547
+ lastActionAt: nowIso(),
548
+ });
549
+ return { session, page: this.getPage(sessionId, page.pageId).page, result };
550
+ }
551
+
552
+ async waitFor(
553
+ sessionId: string,
554
+ pageId: string | undefined,
555
+ input: { selector?: string; text?: string; timeoutMs: number },
556
+ ) {
557
+ const { session, page } = this.getPage(sessionId, pageId);
558
+ const result = await this.deps.adapter.waitFor(
559
+ session.adapterSession,
560
+ input,
561
+ );
562
+ this.updatePage(session.sessionId, page.pageId, {
563
+ url: result.url,
564
+ title: result.title,
565
+ lastObservationAt: nowIso(),
566
+ });
567
+ return { session, page: this.getPage(sessionId, page.pageId).page, result };
568
+ }
569
+
570
+ async wheel(
571
+ sessionId: string,
572
+ pageId: string | undefined,
573
+ input: {
574
+ selector?: string;
575
+ deltaX: number;
576
+ deltaY: number;
577
+ steps: number;
578
+ stepDelayMs: number;
579
+ timeoutMs?: number;
580
+ },
581
+ ) {
582
+ const { session, page } = this.getPage(sessionId, pageId);
583
+ const result = await this.deps.adapter.wheel(session.adapterSession, input);
584
+ this.updatePage(session.sessionId, page.pageId, {
585
+ url: result.url,
586
+ title: result.title,
587
+ lastActionAt: nowIso(),
588
+ lastObservationAt: nowIso(),
589
+ });
590
+ return { session, page: this.getPage(sessionId, page.pageId).page, result };
591
+ }
592
+
593
+ async drag(
594
+ sessionId: string,
595
+ pageId: string | undefined,
596
+ input: {
597
+ fromSelector: string;
598
+ toSelector: string;
599
+ steps: number;
600
+ timeoutMs?: number;
601
+ },
602
+ ) {
603
+ const { session, page } = this.getPage(sessionId, pageId);
604
+ const result = await this.deps.adapter.drag(session.adapterSession, input);
605
+ this.updatePage(session.sessionId, page.pageId, {
606
+ url: result.url,
607
+ title: result.title,
608
+ lastActionAt: nowIso(),
609
+ lastObservationAt: nowIso(),
610
+ });
611
+ return { session, page: this.getPage(sessionId, page.pageId).page, result };
612
+ }
613
+
614
+ async swipe(
615
+ sessionId: string,
616
+ pageId: string | undefined,
617
+ input: {
618
+ selector?: string;
619
+ startX?: number;
620
+ startY?: number;
621
+ deltaX: number;
622
+ deltaY: number;
623
+ speed: number;
624
+ },
625
+ ) {
626
+ const { session, page } = this.getPage(sessionId, pageId);
627
+ const result = await this.deps.adapter.swipe(session.adapterSession, input);
628
+ this.updatePage(session.sessionId, page.pageId, {
629
+ url: result.url,
630
+ title: result.title,
631
+ lastActionAt: nowIso(),
632
+ lastObservationAt: nowIso(),
633
+ });
634
+ return { session, page: this.getPage(sessionId, page.pageId).page, result };
635
+ }
636
+
637
+ async pinch(
638
+ sessionId: string,
639
+ pageId: string | undefined,
640
+ input: {
641
+ selector?: string;
642
+ centerX?: number;
643
+ centerY?: number;
644
+ scaleFactor: number;
645
+ speed: number;
646
+ },
647
+ ) {
648
+ const { session, page } = this.getPage(sessionId, pageId);
649
+ const result = await this.deps.adapter.pinch(session.adapterSession, input);
650
+ this.updatePage(session.sessionId, page.pageId, {
651
+ url: result.url,
652
+ title: result.title,
653
+ lastActionAt: nowIso(),
654
+ lastObservationAt: nowIso(),
655
+ });
656
+ return { session, page: this.getPage(sessionId, page.pageId).page, result };
657
+ }
658
+
659
+ async closeSession(sessionId: string) {
660
+ const session = this.getSession(sessionId);
661
+ session.status = "closing";
662
+ await this.deps.adapter.closeSession(session.adapterSession);
663
+ session.status = "closed";
664
+ this.sessions.set(sessionId, session);
665
+ if (session.userDataDir) {
666
+ this.activeUserDataDirs.delete(session.userDataDir);
667
+ }
668
+ return session;
669
+ }
670
+ }
@@ -0,0 +1,34 @@
1
+ export type SessionRestartDecision = {
2
+ recommended: boolean;
3
+ reason?: string;
4
+ };
5
+
6
+ export function shouldRecommendSessionRestart(input: {
7
+ consecutiveErrors: number;
8
+ maxConsecutiveErrors: number;
9
+ cooldownMs: number;
10
+ lastRestartAt?: string;
11
+ now: string;
12
+ browserError: boolean;
13
+ }) {
14
+ if (!input.browserError && input.consecutiveErrors < input.maxConsecutiveErrors) {
15
+ return { recommended: false } satisfies SessionRestartDecision;
16
+ }
17
+
18
+ if (input.lastRestartAt) {
19
+ const elapsed = Date.parse(input.now) - Date.parse(input.lastRestartAt);
20
+ if (elapsed < input.cooldownMs) {
21
+ return {
22
+ recommended: false,
23
+ reason: "Restart cooldown is still active."
24
+ } satisfies SessionRestartDecision;
25
+ }
26
+ }
27
+
28
+ return {
29
+ recommended: true,
30
+ reason: input.browserError
31
+ ? "Browser/runtime failure suggests a session restart."
32
+ : "Consecutive errors crossed the restart threshold."
33
+ } satisfies SessionRestartDecision;
34
+ }