pi-agent-browser-native 0.2.31 → 0.2.33

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 (66) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +64 -18
  3. package/docs/ARCHITECTURE.md +13 -10
  4. package/docs/COMMAND_REFERENCE.md +71 -16
  5. package/docs/ELECTRON.md +387 -0
  6. package/docs/RELEASE.md +34 -4
  7. package/docs/REQUIREMENTS.md +5 -3
  8. package/docs/SUPPORT_MATRIX.md +36 -21
  9. package/docs/TOOL_CONTRACT.md +198 -40
  10. package/extensions/agent-browser/index.ts +1585 -3486
  11. package/extensions/agent-browser/lib/electron/cleanup.ts +287 -0
  12. package/extensions/agent-browser/lib/electron/discovery.ts +717 -0
  13. package/extensions/agent-browser/lib/electron/launch.ts +553 -0
  14. package/extensions/agent-browser/lib/input-modes/electron.ts +170 -0
  15. package/extensions/agent-browser/lib/input-modes/job.ts +203 -0
  16. package/extensions/agent-browser/lib/input-modes/lookups.ts +447 -0
  17. package/extensions/agent-browser/lib/input-modes/params.ts +188 -0
  18. package/extensions/agent-browser/lib/input-modes/semantic-action.ts +107 -0
  19. package/extensions/agent-browser/lib/input-modes/shared.ts +46 -0
  20. package/extensions/agent-browser/lib/input-modes/types.ts +221 -0
  21. package/extensions/agent-browser/lib/input-modes.ts +41 -0
  22. package/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.ts +696 -0
  23. package/extensions/agent-browser/lib/orchestration/browser-run/final-result.ts +450 -0
  24. package/extensions/agent-browser/lib/orchestration/browser-run/index.ts +46 -0
  25. package/extensions/agent-browser/lib/orchestration/browser-run/prepare.ts +711 -0
  26. package/extensions/agent-browser/lib/orchestration/browser-run/process-output.ts +386 -0
  27. package/extensions/agent-browser/lib/orchestration/browser-run/session-state.ts +868 -0
  28. package/extensions/agent-browser/lib/orchestration/browser-run/types.ts +476 -0
  29. package/extensions/agent-browser/lib/orchestration/browser-run.ts +1 -0
  30. package/extensions/agent-browser/lib/orchestration/input-plan.ts +338 -0
  31. package/extensions/agent-browser/lib/playbook.ts +15 -13
  32. package/extensions/agent-browser/lib/process.ts +106 -4
  33. package/extensions/agent-browser/lib/results/action-recommendations.ts +269 -0
  34. package/extensions/agent-browser/lib/results/artifact-manifest.ts +114 -0
  35. package/extensions/agent-browser/lib/results/artifact-state.ts +13 -0
  36. package/extensions/agent-browser/lib/results/categories.ts +106 -0
  37. package/extensions/agent-browser/lib/results/contracts.ts +220 -0
  38. package/extensions/agent-browser/lib/results/editable-ref-evidence.ts +72 -0
  39. package/extensions/agent-browser/lib/results/envelope.ts +2 -1
  40. package/extensions/agent-browser/lib/results/network.ts +64 -0
  41. package/extensions/agent-browser/lib/results/next-actions.ts +117 -0
  42. package/extensions/agent-browser/lib/results/presentation/artifacts.ts +506 -0
  43. package/extensions/agent-browser/lib/results/presentation/batch.ts +355 -0
  44. package/extensions/agent-browser/lib/results/presentation/common.ts +53 -0
  45. package/extensions/agent-browser/lib/results/presentation/content.ts +36 -0
  46. package/extensions/agent-browser/lib/results/presentation/diagnostics.ts +730 -0
  47. package/extensions/agent-browser/lib/results/presentation/errors.ts +125 -0
  48. package/extensions/agent-browser/lib/results/presentation/large-output.ts +182 -0
  49. package/extensions/agent-browser/lib/results/presentation/navigation.ts +216 -0
  50. package/extensions/agent-browser/lib/results/presentation/registry.ts +154 -0
  51. package/extensions/agent-browser/lib/results/presentation/skills.ts +143 -0
  52. package/extensions/agent-browser/lib/results/presentation.ts +87 -2369
  53. package/extensions/agent-browser/lib/results/recovery-actions.ts +139 -0
  54. package/extensions/agent-browser/lib/results/recovery-next-actions.ts +71 -0
  55. package/extensions/agent-browser/lib/results/selector-recovery.ts +312 -0
  56. package/extensions/agent-browser/lib/results/shared.ts +17 -701
  57. package/extensions/agent-browser/lib/results/snapshot-high-value-controls.ts +262 -0
  58. package/extensions/agent-browser/lib/results/snapshot-refs.ts +100 -0
  59. package/extensions/agent-browser/lib/results/snapshot-segments.ts +366 -0
  60. package/extensions/agent-browser/lib/results/snapshot-spill.ts +63 -0
  61. package/extensions/agent-browser/lib/results/snapshot.ts +37 -489
  62. package/extensions/agent-browser/lib/results/text.ts +40 -0
  63. package/extensions/agent-browser/lib/results.ts +16 -5
  64. package/extensions/agent-browser/lib/session-page-state.ts +486 -0
  65. package/extensions/agent-browser/lib/temp.ts +26 -0
  66. package/package.json +6 -4
@@ -0,0 +1,287 @@
1
+ /**
2
+ * Purpose: Inspect and clean wrapper-owned Electron launch resources.
3
+ * Responsibilities: Report CDP liveness/targets without mutation and remove only tracked process/profile resources during explicit or shutdown cleanup.
4
+ * Scope: Host-side Electron status and resource cleanup only; upstream managed-session close remains in the extension entrypoint.
5
+ * Usage: Called by electron.status, electron.cleanup, and session_shutdown handling.
6
+ * Invariants/Assumptions: Cleanup operates only on launch records produced by this wrapper and prefers partial cleanup reporting over killing or deleting untracked resources.
7
+ */
8
+
9
+ import { execFile, type ChildProcess } from "node:child_process";
10
+ import { access, rm } from "node:fs/promises";
11
+ import { promisify } from "node:util";
12
+
13
+ import { ELECTRON_PROFILE_DIR_PREFIX, type ElectronCdpTarget, type ElectronCdpVersion, type ElectronLaunchRecord } from "./launch.js";
14
+ import { getSecureTempChildDirectoryValidationError } from "../temp.js";
15
+
16
+ const ELECTRON_STATUS_FETCH_TIMEOUT_MS = 1_000;
17
+ const ELECTRON_CLEANUP_DEFAULT_TIMEOUT_MS = 5_000;
18
+ const ELECTRON_CLEANUP_POLL_INTERVAL_MS = 100;
19
+ const RESTORED_PROCESS_COMMAND_TIMEOUT_MS = 1_000;
20
+ const execFileAsync = promisify(execFile);
21
+
22
+ export interface ElectronLaunchStatus {
23
+ cleanupState: ElectronLaunchRecord["cleanupState"];
24
+ launchId: string;
25
+ pid?: number;
26
+ pidAlive?: boolean;
27
+ port: number;
28
+ portAlive: boolean;
29
+ targets: ElectronCdpTarget[];
30
+ version?: ElectronCdpVersion;
31
+ }
32
+
33
+ export interface ElectronCleanupStep {
34
+ error?: string;
35
+ resource: "debug-port" | "managed-session" | "process" | "user-data-dir";
36
+ state: "already-gone" | "failed" | "removed" | "skipped";
37
+ }
38
+
39
+ export interface ElectronCleanupResult {
40
+ launchId: string;
41
+ partial: boolean;
42
+ record: ElectronLaunchRecord;
43
+ remainingResources: string[];
44
+ steps: ElectronCleanupStep[];
45
+ summary: string;
46
+ }
47
+
48
+ function sleep(ms: number): Promise<void> {
49
+ return new Promise((resolve) => setTimeout(resolve, ms));
50
+ }
51
+
52
+ function isRecord(value: unknown): value is Record<string, unknown> {
53
+ return typeof value === "object" && value !== null;
54
+ }
55
+
56
+ function asString(value: unknown): string | undefined {
57
+ return typeof value === "string" && value.trim().length > 0 ? value : undefined;
58
+ }
59
+
60
+ function parseCdpVersion(value: unknown): ElectronCdpVersion | undefined {
61
+ if (!isRecord(value)) return undefined;
62
+ return {
63
+ browser: asString(value.Browser) ?? asString(value.browser),
64
+ protocolVersion: asString(value["Protocol-Version"]) ?? asString(value.protocolVersion),
65
+ userAgent: asString(value["User-Agent"]) ?? asString(value.userAgent),
66
+ v8Version: asString(value["V8-Version"]) ?? asString(value.v8Version),
67
+ webKitVersion: asString(value["WebKit-Version"]) ?? asString(value.webKitVersion),
68
+ webSocketDebuggerUrl: asString(value.webSocketDebuggerUrl),
69
+ };
70
+ }
71
+
72
+ function parseCdpTargets(value: unknown): ElectronCdpTarget[] {
73
+ if (!Array.isArray(value)) return [];
74
+ return value.filter(isRecord).map((target) => ({
75
+ id: asString(target.id),
76
+ title: asString(target.title),
77
+ type: asString(target.type),
78
+ url: asString(target.url),
79
+ webSocketDebuggerUrl: asString(target.webSocketDebuggerUrl),
80
+ }));
81
+ }
82
+
83
+ async function fetchJson(url: string): Promise<unknown | undefined> {
84
+ const controller = new AbortController();
85
+ const timeout = setTimeout(() => controller.abort(), ELECTRON_STATUS_FETCH_TIMEOUT_MS);
86
+ try {
87
+ const response = await fetch(url, { signal: controller.signal });
88
+ if (!response.ok) return undefined;
89
+ return await response.json() as unknown;
90
+ } catch {
91
+ return undefined;
92
+ } finally {
93
+ clearTimeout(timeout);
94
+ }
95
+ }
96
+
97
+ function isPidAlive(pid: number | undefined): boolean | undefined {
98
+ if (!pid || !Number.isSafeInteger(pid) || pid <= 0) return undefined;
99
+ try {
100
+ process.kill(pid, 0);
101
+ return true;
102
+ } catch (error) {
103
+ const code = (error as NodeJS.ErrnoException).code;
104
+ return code === "EPERM" ? true : false;
105
+ }
106
+ }
107
+
108
+ async function pathExists(path: string): Promise<boolean> {
109
+ try {
110
+ await access(path);
111
+ return true;
112
+ } catch {
113
+ return false;
114
+ }
115
+ }
116
+
117
+ async function isPortAlive(port: number): Promise<{ targets: ElectronCdpTarget[]; version?: ElectronCdpVersion }> {
118
+ const version = parseCdpVersion(await fetchJson(`http://127.0.0.1:${port}/json/version`));
119
+ if (!version) return { targets: [] };
120
+ const targets = parseCdpTargets(await fetchJson(`http://127.0.0.1:${port}/json/list`));
121
+ return { targets, version };
122
+ }
123
+
124
+ export async function inspectElectronLaunchStatus(record: ElectronLaunchRecord): Promise<ElectronLaunchStatus> {
125
+ const cdp = await isPortAlive(record.port);
126
+ return {
127
+ cleanupState: record.cleanupState,
128
+ launchId: record.launchId,
129
+ pid: record.pid,
130
+ pidAlive: isPidAlive(record.pid),
131
+ port: record.port,
132
+ portAlive: cdp.version !== undefined,
133
+ targets: cdp.targets,
134
+ version: cdp.version,
135
+ };
136
+ }
137
+
138
+ async function waitForProcessExit(child: ChildProcess | undefined, pid: number | undefined, deadlineMs: number): Promise<boolean> {
139
+ while (Date.now() <= deadlineMs) {
140
+ if (child && (child.exitCode !== null || child.signalCode !== null)) return true;
141
+ if (isPidAlive(pid) === false) return true;
142
+ await sleep(ELECTRON_CLEANUP_POLL_INTERVAL_MS);
143
+ }
144
+ return isPidAlive(pid) === false;
145
+ }
146
+
147
+ async function readPidCommandLine(pid: number | undefined): Promise<string | undefined> {
148
+ if (!pid || !Number.isSafeInteger(pid) || pid <= 0) return undefined;
149
+ try {
150
+ const { stdout } = await execFileAsync("ps", ["-ww", "-p", String(pid), "-o", "command="], {
151
+ timeout: RESTORED_PROCESS_COMMAND_TIMEOUT_MS,
152
+ });
153
+ return stdout.trim() || undefined;
154
+ } catch {
155
+ return undefined;
156
+ }
157
+ }
158
+
159
+ function restoredLaunchCommandMatchesRecord(record: ElectronLaunchRecord, commandLine: string | undefined): boolean {
160
+ return commandLine?.includes(`--user-data-dir=${record.userDataDir}`) === true;
161
+ }
162
+
163
+ async function getRestoredProcessVerificationError(record: ElectronLaunchRecord): Promise<string | undefined> {
164
+ const commandLine = await readPidCommandLine(record.pid);
165
+ if (!commandLine) {
166
+ return `PID ${record.pid} is alive, but this session has no tracked child handle and its command line could not be inspected; refusing to signal a restored PID that may have been reused.`;
167
+ }
168
+ if (!restoredLaunchCommandMatchesRecord(record, commandLine)) {
169
+ return `PID ${record.pid} is alive, but this session has no tracked child handle and its command line does not include wrapper-owned user data dir ${record.userDataDir}; refusing to signal a restored PID that may have been reused.`;
170
+ }
171
+ return undefined;
172
+ }
173
+
174
+ function killPid(pid: number | undefined, signal: NodeJS.Signals): boolean {
175
+ if (!pid || !Number.isSafeInteger(pid) || pid <= 0) return false;
176
+ try {
177
+ process.kill(pid, signal);
178
+ return true;
179
+ } catch {
180
+ return false;
181
+ }
182
+ }
183
+
184
+ function killProcessGroup(processGroupId: number | undefined, signal: NodeJS.Signals): boolean {
185
+ if (process.platform === "win32" || !processGroupId || !Number.isSafeInteger(processGroupId) || processGroupId <= 0) return false;
186
+ try {
187
+ process.kill(-processGroupId, signal);
188
+ return true;
189
+ } catch {
190
+ return false;
191
+ }
192
+ }
193
+
194
+ function signalRestoredLaunchProcess(record: ElectronLaunchRecord, signal: NodeJS.Signals): boolean {
195
+ return killProcessGroup(record.processGroupId, signal) || killPid(record.pid, signal);
196
+ }
197
+
198
+ async function cleanupProcess(record: ElectronLaunchRecord, child: ChildProcess | undefined, deadlineMs: number): Promise<ElectronCleanupStep> {
199
+ if (!record.pid) return { resource: "process", state: "skipped" };
200
+ if (isPidAlive(record.pid) === false) return { resource: "process", state: "already-gone" };
201
+ if (!child) {
202
+ const verificationError = await getRestoredProcessVerificationError(record);
203
+ if (verificationError) return { error: verificationError, resource: "process", state: "failed" };
204
+ if (!signalRestoredLaunchProcess(record, "SIGTERM")) {
205
+ return { error: `PID ${record.pid} matched wrapper launch metadata but could not be signaled.`, resource: "process", state: "failed" };
206
+ }
207
+ if (await waitForProcessExit(undefined, record.pid, deadlineMs)) return { resource: "process", state: "removed" };
208
+ signalRestoredLaunchProcess(record, "SIGKILL");
209
+ if (await waitForProcessExit(undefined, record.pid, Date.now() + 1_000)) return { resource: "process", state: "removed" };
210
+ return { error: `PID ${record.pid} remained alive after SIGTERM/SIGKILL.`, resource: "process", state: "failed" };
211
+ }
212
+ if (child.exitCode === null && child.signalCode === null) child.kill("SIGTERM");
213
+ else killPid(record.pid, "SIGTERM");
214
+ if (await waitForProcessExit(child, record.pid, deadlineMs)) return { resource: "process", state: "removed" };
215
+ if (child.exitCode === null && child.signalCode === null) child.kill("SIGKILL");
216
+ else killPid(record.pid, "SIGKILL");
217
+ if (await waitForProcessExit(child, record.pid, Date.now() + 1_000)) return { resource: "process", state: "removed" };
218
+ return { error: `PID ${record.pid} remained alive after SIGTERM/SIGKILL.`, resource: "process", state: "failed" };
219
+ }
220
+
221
+ async function cleanupUserDataDir(record: ElectronLaunchRecord): Promise<ElectronCleanupStep> {
222
+ if (!record.userDataDir) return { resource: "user-data-dir", state: "skipped" };
223
+ if (!await pathExists(record.userDataDir)) return { resource: "user-data-dir", state: "already-gone" };
224
+ const validationError = await getSecureTempChildDirectoryValidationError(record.userDataDir, ELECTRON_PROFILE_DIR_PREFIX);
225
+ if (validationError) return { error: validationError, resource: "user-data-dir", state: "failed" };
226
+ try {
227
+ await rm(record.userDataDir, { force: true, recursive: true });
228
+ return { resource: "user-data-dir", state: await pathExists(record.userDataDir) ? "failed" : "removed" };
229
+ } catch (error) {
230
+ return { error: error instanceof Error ? error.message : String(error), resource: "user-data-dir", state: "failed" };
231
+ }
232
+ }
233
+
234
+ function shouldSkipUserDataDirCleanup(processStep: ElectronCleanupStep, debugPortStep: ElectronCleanupStep): string | undefined {
235
+ if (processStep.state === "failed") return `Skipped because process cleanup failed: ${processStep.error ?? "process state could not be verified"}.`;
236
+ if (debugPortStep.state === "failed") return `Skipped because debug port cleanup is incomplete: ${debugPortStep.error ?? "debug port still responds"}.`;
237
+ return undefined;
238
+ }
239
+
240
+ async function cleanupDebugPort(record: ElectronLaunchRecord): Promise<ElectronCleanupStep> {
241
+ const cdp = await isPortAlive(record.port);
242
+ return cdp.version ? { resource: "debug-port", state: "failed", error: `/json/version still responds on port ${record.port}.` } : { resource: "debug-port", state: "already-gone" };
243
+ }
244
+
245
+ function summarizeCleanup(launchId: string, steps: ElectronCleanupStep[]): { partial: boolean; remainingResources: string[]; summary: string } {
246
+ const remainingResources = steps
247
+ .filter((step) => step.state === "failed" || (step.resource === "user-data-dir" && step.state === "skipped" && step.error))
248
+ .map((step) => step.resource);
249
+ const partial = remainingResources.length > 0;
250
+ return {
251
+ partial,
252
+ remainingResources,
253
+ summary: partial
254
+ ? `Electron cleanup for ${launchId} is partial; remaining resources: ${remainingResources.join(", ")}.`
255
+ : `Electron cleanup for ${launchId} completed.`,
256
+ };
257
+ }
258
+
259
+ export async function cleanupElectronLaunchResources(options: {
260
+ child?: ChildProcess;
261
+ record: ElectronLaunchRecord;
262
+ timeoutMs?: number;
263
+ }): Promise<ElectronCleanupResult> {
264
+ const timeoutMs = Number.isSafeInteger(options.timeoutMs) && (options.timeoutMs ?? 0) > 0
265
+ ? options.timeoutMs as number
266
+ : ELECTRON_CLEANUP_DEFAULT_TIMEOUT_MS;
267
+ const deadlineMs = Date.now() + timeoutMs;
268
+ const processStep = await cleanupProcess(options.record, options.child, deadlineMs);
269
+ const debugPortStep = await cleanupDebugPort(options.record);
270
+ const userDataDirSkipReason = shouldSkipUserDataDirCleanup(processStep, debugPortStep);
271
+ const userDataDirStep = userDataDirSkipReason
272
+ ? { error: userDataDirSkipReason, resource: "user-data-dir" as const, state: "skipped" as const }
273
+ : await cleanupUserDataDir(options.record);
274
+ const steps = [processStep, debugPortStep, userDataDirStep];
275
+ const summary = summarizeCleanup(options.record.launchId, steps);
276
+ return {
277
+ launchId: options.record.launchId,
278
+ partial: summary.partial,
279
+ record: {
280
+ ...options.record,
281
+ cleanupState: summary.partial ? "partial" : "cleaned",
282
+ },
283
+ remainingResources: summary.remainingResources,
284
+ steps,
285
+ summary: summary.summary,
286
+ };
287
+ }