pi-agent-browser-native 0.2.32 → 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.
- package/CHANGELOG.md +17 -0
- package/README.md +27 -16
- package/docs/ARCHITECTURE.md +3 -2
- package/docs/COMMAND_REFERENCE.md +18 -10
- package/docs/ELECTRON.md +23 -4
- package/docs/RELEASE.md +4 -2
- package/docs/REQUIREMENTS.md +1 -1
- package/docs/SUPPORT_MATRIX.md +28 -16
- package/docs/TOOL_CONTRACT.md +29 -24
- package/extensions/agent-browser/index.ts +404 -4371
- package/extensions/agent-browser/lib/input-modes/electron.ts +170 -0
- package/extensions/agent-browser/lib/input-modes/job.ts +203 -0
- package/extensions/agent-browser/lib/input-modes/lookups.ts +447 -0
- package/extensions/agent-browser/lib/input-modes/params.ts +188 -0
- package/extensions/agent-browser/lib/input-modes/semantic-action.ts +107 -0
- package/extensions/agent-browser/lib/input-modes/shared.ts +46 -0
- package/extensions/agent-browser/lib/input-modes/types.ts +221 -0
- package/extensions/agent-browser/lib/input-modes.ts +41 -0
- package/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.ts +696 -0
- package/extensions/agent-browser/lib/orchestration/browser-run/final-result.ts +450 -0
- package/extensions/agent-browser/lib/orchestration/browser-run/index.ts +46 -0
- package/extensions/agent-browser/lib/orchestration/browser-run/prepare.ts +711 -0
- package/extensions/agent-browser/lib/orchestration/browser-run/process-output.ts +386 -0
- package/extensions/agent-browser/lib/orchestration/browser-run/session-state.ts +868 -0
- package/extensions/agent-browser/lib/orchestration/browser-run/types.ts +476 -0
- package/extensions/agent-browser/lib/orchestration/browser-run.ts +1 -0
- package/extensions/agent-browser/lib/orchestration/input-plan.ts +338 -0
- package/extensions/agent-browser/lib/playbook.ts +12 -11
- package/extensions/agent-browser/lib/process.ts +106 -4
- package/extensions/agent-browser/lib/results/action-recommendations.ts +269 -0
- package/extensions/agent-browser/lib/results/artifact-manifest.ts +114 -0
- package/extensions/agent-browser/lib/results/artifact-state.ts +13 -0
- package/extensions/agent-browser/lib/results/categories.ts +106 -0
- package/extensions/agent-browser/lib/results/contracts.ts +220 -0
- package/extensions/agent-browser/lib/results/editable-ref-evidence.ts +72 -0
- package/extensions/agent-browser/lib/results/envelope.ts +2 -1
- package/extensions/agent-browser/lib/results/network.ts +64 -0
- package/extensions/agent-browser/lib/results/next-actions.ts +117 -0
- package/extensions/agent-browser/lib/results/presentation/artifacts.ts +506 -0
- package/extensions/agent-browser/lib/results/presentation/batch.ts +355 -0
- package/extensions/agent-browser/lib/results/presentation/common.ts +53 -0
- package/extensions/agent-browser/lib/results/presentation/content.ts +36 -0
- package/extensions/agent-browser/lib/results/presentation/diagnostics.ts +730 -0
- package/extensions/agent-browser/lib/results/presentation/errors.ts +125 -0
- package/extensions/agent-browser/lib/results/presentation/large-output.ts +182 -0
- package/extensions/agent-browser/lib/results/presentation/navigation.ts +216 -0
- package/extensions/agent-browser/lib/results/presentation/registry.ts +154 -0
- package/extensions/agent-browser/lib/results/presentation/skills.ts +143 -0
- package/extensions/agent-browser/lib/results/presentation.ts +87 -2399
- package/extensions/agent-browser/lib/results/recovery-actions.ts +139 -0
- package/extensions/agent-browser/lib/results/recovery-next-actions.ts +71 -0
- package/extensions/agent-browser/lib/results/selector-recovery.ts +312 -0
- package/extensions/agent-browser/lib/results/shared.ts +17 -789
- package/extensions/agent-browser/lib/results/snapshot-high-value-controls.ts +262 -0
- package/extensions/agent-browser/lib/results/snapshot-refs.ts +100 -0
- package/extensions/agent-browser/lib/results/snapshot-segments.ts +366 -0
- package/extensions/agent-browser/lib/results/snapshot-spill.ts +63 -0
- package/extensions/agent-browser/lib/results/snapshot.ts +37 -489
- package/extensions/agent-browser/lib/results/text.ts +40 -0
- package/extensions/agent-browser/lib/results.ts +16 -5
- package/extensions/agent-browser/lib/session-page-state.ts +486 -0
- package/package.json +2 -1
|
@@ -0,0 +1,730 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Purpose: Render diagnostic command families and safe redacted diagnostic data.
|
|
3
|
+
* Responsibilities: Format sessions, profiles, auth/cookies/storage, network diagnostics, console/errors, stream/dashboard/chat, and build network follow-up actions.
|
|
4
|
+
* Scope: Diagnostic/result-state command presentation only; core orchestration stays in presentation.ts.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { isRecord } from "../../parsing.js";
|
|
8
|
+
import { redactSensitiveText, redactSensitiveValue, type CommandInfo } from "../../runtime.js";
|
|
9
|
+
import type { AgentBrowserNextAction } from "../contracts.js";
|
|
10
|
+
import { classifyNetworkRequestFailure, summarizeNetworkFailures } from "../network.js";
|
|
11
|
+
import { withOptionalSessionArgs } from "../next-actions.js";
|
|
12
|
+
import { stringifyUnknown, truncateText } from "../text.js";
|
|
13
|
+
import {
|
|
14
|
+
firstLine,
|
|
15
|
+
formatCount,
|
|
16
|
+
getArrayField,
|
|
17
|
+
getStringField,
|
|
18
|
+
parseJsonPreviewString,
|
|
19
|
+
redactModelFacingText,
|
|
20
|
+
redactModelFacingTextIfSensitive,
|
|
21
|
+
stringifyModelFacing,
|
|
22
|
+
} from "./common.js";
|
|
23
|
+
|
|
24
|
+
const DIAGNOSTIC_REQUEST_PREVIEW_LIMIT = 40;
|
|
25
|
+
|
|
26
|
+
const DIAGNOSTIC_LOG_PREVIEW_LIMIT = 80;
|
|
27
|
+
|
|
28
|
+
const NETWORK_BODY_PREVIEW_MAX_CHARS = 280;
|
|
29
|
+
|
|
30
|
+
const NETWORK_ERROR_PREVIEW_MAX_CHARS = 220;
|
|
31
|
+
|
|
32
|
+
const NETWORK_NEXT_ACTION_LIMIT = 4;
|
|
33
|
+
|
|
34
|
+
const NETWORK_FILTER_MAX_CHARS = 160;
|
|
35
|
+
|
|
36
|
+
const NETWORK_FILTER_SENSITIVE_SEGMENT_TERMS = [
|
|
37
|
+
"apikey",
|
|
38
|
+
"api-key",
|
|
39
|
+
"api_key",
|
|
40
|
+
"authentication",
|
|
41
|
+
"authorization",
|
|
42
|
+
"bearer",
|
|
43
|
+
"credential",
|
|
44
|
+
"credentials",
|
|
45
|
+
"jwt",
|
|
46
|
+
"passwd",
|
|
47
|
+
"password",
|
|
48
|
+
"reset",
|
|
49
|
+
"secret",
|
|
50
|
+
"session",
|
|
51
|
+
"token",
|
|
52
|
+
] as const;
|
|
53
|
+
|
|
54
|
+
const SENSITIVE_PRESENTATION_FIELD_PATTERN = /^(?:access(?:_|-)?token|api(?:_|-)?key|auth(?:orization)?|bearer|client(?:_|-)?secret|cookie|id(?:_|-)?token|pass(?:word)?|proxy(?:_|-)?authorization|refresh(?:_|-)?token|secret|session(?:_|-)?id|set(?:_|-)?cookie|sig(?:nature)?|token|x(?:_|-)?api(?:_|-)?key)$/i;
|
|
55
|
+
|
|
56
|
+
const NETWORK_FILTER_OPAQUE_SEGMENT_PATTERN = /^(?:[A-Fa-f0-9]{16,}|(?=.*[A-Za-z])(?=.*\d)[A-Za-z0-9_-]{16,})$/;
|
|
57
|
+
|
|
58
|
+
const NETWORK_PREVIEW_FIELD_CANDIDATES = {
|
|
59
|
+
request: ["postData"] as const,
|
|
60
|
+
response: ["responseBody"] as const,
|
|
61
|
+
error: ["error", "failureText", "errorText"] as const,
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const AUTH_SHOW_SAFE_FIELDS = ["name", "profile", "url", "username", "createdAt", "updatedAt"] as const;
|
|
65
|
+
|
|
66
|
+
export function getTabSummary(data: Record<string, unknown>): string | undefined {
|
|
67
|
+
const tabs = Array.isArray(data.tabs) ? data.tabs : undefined;
|
|
68
|
+
if (!tabs) return undefined;
|
|
69
|
+
|
|
70
|
+
const lines = tabs.map((tab, index) => {
|
|
71
|
+
if (!isRecord(tab)) return `${index}: <invalid tab>`;
|
|
72
|
+
const marker = tab.active === true ? "*" : "-";
|
|
73
|
+
const title = typeof tab.title === "string" ? tab.title : "(untitled)";
|
|
74
|
+
const url = typeof tab.url === "string" ? tab.url : "(no url)";
|
|
75
|
+
const tabSelector =
|
|
76
|
+
typeof tab.tabId === "string" && tab.tabId.trim().length > 0
|
|
77
|
+
? tab.tabId.trim()
|
|
78
|
+
: typeof tab.label === "string" && tab.label.trim().length > 0
|
|
79
|
+
? tab.label.trim()
|
|
80
|
+
: typeof tab.index === "number"
|
|
81
|
+
? String(tab.index)
|
|
82
|
+
: String(index);
|
|
83
|
+
return `${marker} [${tabSelector}] ${title} — ${url}`;
|
|
84
|
+
});
|
|
85
|
+
return lines.join("\n");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function getStreamSummary(data: Record<string, unknown>): string | undefined {
|
|
89
|
+
if (typeof data.enabled !== "boolean" || typeof data.connected !== "boolean") {
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const lines = [
|
|
94
|
+
`Enabled: ${data.enabled}`,
|
|
95
|
+
`Connected: ${data.connected}`,
|
|
96
|
+
`Screencasting: ${data.screencasting === true}`,
|
|
97
|
+
];
|
|
98
|
+
if (typeof data.port === "number") {
|
|
99
|
+
lines.push(`Port: ${data.port}`);
|
|
100
|
+
lines.push(`WebSocket URL: ${getStreamWebSocketUrl(data.port)}`);
|
|
101
|
+
lines.push(`Frame format: JSON messages with base64 JPEG frame data`);
|
|
102
|
+
}
|
|
103
|
+
return lines.join("\n");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function getStreamWebSocketUrl(port: number): string {
|
|
107
|
+
return `ws://127.0.0.1:${port}`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function enrichStreamStatusData(commandInfo: CommandInfo, data: unknown): unknown {
|
|
111
|
+
if (commandInfo.command !== "stream" || commandInfo.subcommand !== "status" || !isRecord(data) || typeof data.port !== "number") {
|
|
112
|
+
return data;
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
...data,
|
|
116
|
+
frameFormat: "JSON messages with base64 JPEG frame data",
|
|
117
|
+
wsUrl: getStreamWebSocketUrl(data.port),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function formatDiagnosticSummary(commandInfo: CommandInfo, data: Record<string, unknown>): string | undefined {
|
|
122
|
+
if (commandInfo.command === "session") {
|
|
123
|
+
const sessions = getArrayField(data, "sessions");
|
|
124
|
+
if (sessions) return `Sessions: ${sessions.length}`;
|
|
125
|
+
const session = getStringField(data, "session");
|
|
126
|
+
if (session) return `Session: ${session}`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (commandInfo.command === "profiles") {
|
|
130
|
+
const profiles = getArrayField(data, "profiles");
|
|
131
|
+
if (profiles) return `Chrome profiles: ${profiles.length}`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (commandInfo.command === "auth") {
|
|
135
|
+
const profiles = getArrayField(data, "profiles");
|
|
136
|
+
if (profiles) return `Auth profiles: ${profiles.length}`;
|
|
137
|
+
const name = getStringField(data, "name") ?? getStringField(data, "profile") ?? commandInfo.subcommand;
|
|
138
|
+
if (name && commandInfo.subcommand === "show") return `Auth profile: ${name}`;
|
|
139
|
+
if (name && ["save", "login", "delete"].includes(commandInfo.subcommand ?? "")) return `Auth ${commandInfo.subcommand}: ${name}`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (commandInfo.command === "cookies") {
|
|
143
|
+
const cookies = getArrayField(data, "cookies");
|
|
144
|
+
if (cookies) return `Cookies: ${cookies.length}`;
|
|
145
|
+
const name = getStringField(data, "name");
|
|
146
|
+
if (name) return name;
|
|
147
|
+
if (data.set === true) return "Cookie set";
|
|
148
|
+
if (data.cleared === true || data.clear === true) return "Cookies cleared";
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (commandInfo.command === "storage") {
|
|
152
|
+
const entries = getArrayField(data, "entries") ?? getArrayField(data, "items");
|
|
153
|
+
if (entries) return `Storage entries: ${entries.length}`;
|
|
154
|
+
const key = getStringField(data, "key");
|
|
155
|
+
if (key && (commandInfo.subcommand === "set" || data.set === true || Object.hasOwn(data, "value"))) return `Storage set: ${key}`;
|
|
156
|
+
if (data.cleared === true || data.clear === true) return "Storage cleared";
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (commandInfo.command === "dialog") {
|
|
160
|
+
const open = typeof data.open === "boolean" ? data.open : undefined;
|
|
161
|
+
if (open !== undefined) return open ? "Dialog open" : "No dialog open";
|
|
162
|
+
if (data.accepted === true) return "Dialog accepted";
|
|
163
|
+
if (data.dismissed === true) return "Dialog dismissed";
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (commandInfo.command === "frame") {
|
|
167
|
+
const frame = getStringField(data, "frame") ?? getStringField(data, "name") ?? getStringField(data, "selector") ?? commandInfo.subcommand;
|
|
168
|
+
if (frame) return `Frame: ${frame}`;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (commandInfo.command === "state") {
|
|
172
|
+
const states = getArrayField(data, "states") ?? getArrayField(data, "files");
|
|
173
|
+
if (states) return `States: ${states.length}`;
|
|
174
|
+
if (commandInfo.subcommand === "load") return undefined;
|
|
175
|
+
const stateName = getStringField(data, "name") ?? getStringField(data, "file") ?? getStringField(data, "path") ?? commandInfo.subcommand;
|
|
176
|
+
if (stateName) return `State ${commandInfo.subcommand ?? "result"}: ${stateName}`;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (commandInfo.command === "network") {
|
|
180
|
+
if (commandInfo.subcommand === "requests") {
|
|
181
|
+
const requests = getArrayField(data, "requests");
|
|
182
|
+
if (requests) return `Network requests: ${requests.length}`;
|
|
183
|
+
}
|
|
184
|
+
if (commandInfo.subcommand === "route") {
|
|
185
|
+
const routed = getStringField(data, "routed") ?? getStringField(data, "url") ?? getStringField(data, "pattern");
|
|
186
|
+
return routed ? `Network route: ${redactModelFacingTextIfSensitive(routed)}` : "Network route configured";
|
|
187
|
+
}
|
|
188
|
+
if (commandInfo.subcommand === "unroute") {
|
|
189
|
+
const unrouted = getStringField(data, "unrouted") ?? getStringField(data, "url") ?? getStringField(data, "pattern");
|
|
190
|
+
return unrouted ? `Network unroute: ${redactModelFacingTextIfSensitive(unrouted)}` : "Network route removed";
|
|
191
|
+
}
|
|
192
|
+
if (commandInfo.subcommand === "har") {
|
|
193
|
+
const state = getStringField(data, "state") ?? getStringField(data, "status") ?? commandInfo.subcommand;
|
|
194
|
+
return `Network HAR: ${state}`;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (commandInfo.command === "diff") {
|
|
199
|
+
if (commandInfo.subcommand === "snapshot") return "Snapshot diff completed";
|
|
200
|
+
if (commandInfo.subcommand === "url") return "URL diff completed";
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (["trace", "profiler"].includes(commandInfo.command ?? "")) {
|
|
204
|
+
const state = getStringField(data, "state") ?? getStringField(data, "status") ?? commandInfo.subcommand;
|
|
205
|
+
if (state) return `${commandInfo.command === "trace" ? "Trace" : "Profiler"}: ${state}`;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (commandInfo.command === "highlight") return "Element highlighted";
|
|
209
|
+
if (commandInfo.command === "inspect") return "DevTools inspect opened";
|
|
210
|
+
if (commandInfo.command === "clipboard") return `Clipboard ${commandInfo.subcommand ?? "completed"}`;
|
|
211
|
+
|
|
212
|
+
if (commandInfo.command === "stream") {
|
|
213
|
+
if (commandInfo.subcommand === "enable") {
|
|
214
|
+
const port = typeof data.port === "number" ? ` on port ${data.port}` : "";
|
|
215
|
+
return `Stream enabled${port}`;
|
|
216
|
+
}
|
|
217
|
+
if (commandInfo.subcommand === "disable") return "Stream disabled";
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (commandInfo.command === "chat") return "Chat response";
|
|
221
|
+
|
|
222
|
+
if (commandInfo.command === "console") {
|
|
223
|
+
const messages = getArrayField(data, "messages");
|
|
224
|
+
if (messages) return `Console messages: ${messages.length}`;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (commandInfo.command === "errors") {
|
|
228
|
+
const errors = getArrayField(data, "errors");
|
|
229
|
+
if (errors) return `Page errors: ${errors.length}`;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (commandInfo.command === "dashboard") {
|
|
233
|
+
if (typeof data.port === "number") return `Dashboard running on port ${data.port}`;
|
|
234
|
+
if (data.stopped === true) return "Dashboard stopped";
|
|
235
|
+
if (data.stopped === false) {
|
|
236
|
+
const reason = getStringField(data, "reason");
|
|
237
|
+
return reason ? `Dashboard not stopped: ${reason}` : "Dashboard not stopped";
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (commandInfo.command === "doctor") {
|
|
242
|
+
const status = getStringField(data, "status") ?? getStringField(data, "result");
|
|
243
|
+
if (status) return `Doctor: ${status}`;
|
|
244
|
+
const checks = getArrayField(data, "checks") ?? getArrayField(data, "issues") ?? getArrayField(data, "problems");
|
|
245
|
+
if (checks) return `Doctor: ${formatCount(checks.length, "item")}`;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return undefined;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function formatSessionText(data: Record<string, unknown>): string | undefined {
|
|
252
|
+
const sessions = getArrayField(data, "sessions");
|
|
253
|
+
if (sessions) {
|
|
254
|
+
if (sessions.length === 0) return "No active sessions.";
|
|
255
|
+
return sessions
|
|
256
|
+
.map((item, index) => {
|
|
257
|
+
if (!isRecord(item)) return `${index + 1}. ${stringifyModelFacing(item)}`;
|
|
258
|
+
const name = redactModelFacingText(getStringField(item, "name") ?? getStringField(item, "session") ?? getStringField(item, "id") ?? `(session ${index + 1})`);
|
|
259
|
+
const active = item.active === true ? " *active*" : "";
|
|
260
|
+
const details = [getStringField(item, "url"), getStringField(item, "title")]
|
|
261
|
+
.flatMap((detail) => (detail ? [redactModelFacingTextIfSensitive(detail)] : []))
|
|
262
|
+
.join(" — ");
|
|
263
|
+
return details ? `${index + 1}. ${name}${active} — ${details}` : `${index + 1}. ${name}${active}`;
|
|
264
|
+
})
|
|
265
|
+
.join("\n");
|
|
266
|
+
}
|
|
267
|
+
const session = getStringField(data, "session");
|
|
268
|
+
return session ? `Current session: ${redactModelFacingText(session)}` : undefined;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export function formatProfilesText(profiles: unknown[], label: string): string {
|
|
272
|
+
if (profiles.length === 0) return `No ${label}.`;
|
|
273
|
+
return profiles
|
|
274
|
+
.map((item, index) => {
|
|
275
|
+
if (!isRecord(item)) return `${index + 1}. ${stringifyModelFacing(item)}`;
|
|
276
|
+
const name = redactModelFacingText(getStringField(item, "name") ?? getStringField(item, "profile") ?? `(unnamed ${index + 1})`);
|
|
277
|
+
const directory = getStringField(item, "directory") ?? getStringField(item, "path");
|
|
278
|
+
return directory ? `${index + 1}. ${name} (${redactModelFacingText(directory)})` : `${index + 1}. ${name}`;
|
|
279
|
+
})
|
|
280
|
+
.join("\n");
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function formatAuthShowText(data: Record<string, unknown>): string | undefined {
|
|
284
|
+
const lines = AUTH_SHOW_SAFE_FIELDS.flatMap((key) => {
|
|
285
|
+
const value = data[key];
|
|
286
|
+
return typeof value === "string" && value.trim().length > 0 ? [`${key}: ${redactModelFacingText(value.trim())}`] : [];
|
|
287
|
+
});
|
|
288
|
+
return lines.length > 0 ? lines.join("\n") : undefined;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function getPreviewCandidate(item: Record<string, unknown>, keys: readonly string[]): unknown {
|
|
292
|
+
for (const key of keys) {
|
|
293
|
+
const value = item[key];
|
|
294
|
+
if (value !== undefined && value !== null && value !== "") return value;
|
|
295
|
+
}
|
|
296
|
+
return undefined;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function formatNetworkPreviewValue(value: unknown, maxChars: number): string | undefined {
|
|
300
|
+
if (value === undefined || value === null) return undefined;
|
|
301
|
+
const previewValue = typeof value === "string" ? parseJsonPreviewString(value) : value;
|
|
302
|
+
const redacted = redactSensitiveValue(previewValue);
|
|
303
|
+
const raw = typeof redacted === "string" ? redacted : stringifyUnknown(redacted);
|
|
304
|
+
const normalized = raw.replace(/\s+/g, " ").trim();
|
|
305
|
+
if (normalized.length === 0) return undefined;
|
|
306
|
+
return truncateText(redactSensitiveText(normalized), maxChars);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function appendNetworkPreview(lines: string[], label: string, value: unknown, maxChars: number): void {
|
|
310
|
+
const preview = formatNetworkPreviewValue(value, maxChars);
|
|
311
|
+
if (!preview) return;
|
|
312
|
+
lines.push(` ${label}: ${preview}`);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function formatNetworkRequestLine(item: Record<string, unknown>, index: number): string[] {
|
|
316
|
+
const method = getStringField(item, "method") ?? "GET";
|
|
317
|
+
const status = typeof item.status === "number" ? String(item.status) : "pending";
|
|
318
|
+
const type = getStringField(item, "resourceType") ?? getStringField(item, "mimeType");
|
|
319
|
+
const url = getStringField(item, "url") ?? "(no url)";
|
|
320
|
+
const requestId = getStringField(item, "requestId") ?? getStringField(item, "id");
|
|
321
|
+
const idText = requestId ? ` [${redactSensitiveText(requestId)}]` : "";
|
|
322
|
+
const failureClassification = classifyNetworkRequestFailure(item);
|
|
323
|
+
const impactText = failureClassification ? ` [${failureClassification.impact}: ${failureClassification.reason}]` : "";
|
|
324
|
+
const lines = [`${index + 1}. ${status} ${method} ${truncateText(redactSensitiveText(url), 180)}${type ? ` (${type})` : ""}${idText}${impactText}`];
|
|
325
|
+
appendNetworkPreview(lines, "Payload", getPreviewCandidate(item, NETWORK_PREVIEW_FIELD_CANDIDATES.request), NETWORK_BODY_PREVIEW_MAX_CHARS);
|
|
326
|
+
appendNetworkPreview(lines, "Response", getPreviewCandidate(item, NETWORK_PREVIEW_FIELD_CANDIDATES.response), NETWORK_BODY_PREVIEW_MAX_CHARS);
|
|
327
|
+
appendNetworkPreview(lines, "Error", getPreviewCandidate(item, NETWORK_PREVIEW_FIELD_CANDIDATES.error), NETWORK_ERROR_PREVIEW_MAX_CHARS);
|
|
328
|
+
return lines;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function formatNetworkRequestsText(data: Record<string, unknown>): string | undefined {
|
|
332
|
+
const requests = getArrayField(data, "requests");
|
|
333
|
+
if (!requests) return undefined;
|
|
334
|
+
if (requests.length === 0) return "No network requests captured.";
|
|
335
|
+
const networkFailureSummary = summarizeNetworkFailures(requests);
|
|
336
|
+
const shown = networkFailureSummary.totalCount > 0
|
|
337
|
+
? [`Network failure summary: ${networkFailureSummary.actionableCount} actionable, ${networkFailureSummary.benignCount} benign low-impact (${networkFailureSummary.totalCount} total).`]
|
|
338
|
+
: [];
|
|
339
|
+
const indexedRequests = requests.map((item, index) => ({ index, item }));
|
|
340
|
+
const failedRequests: typeof indexedRequests = [];
|
|
341
|
+
const normalRequests: typeof indexedRequests = [];
|
|
342
|
+
for (const indexed of indexedRequests) {
|
|
343
|
+
if (isRecord(indexed.item) && classifyNetworkRequestFailure(indexed.item)) failedRequests.push(indexed);
|
|
344
|
+
else normalRequests.push(indexed);
|
|
345
|
+
}
|
|
346
|
+
failedRequests.sort((left, right) => {
|
|
347
|
+
const leftClassification = isRecord(left.item) ? classifyNetworkRequestFailure(left.item) : undefined;
|
|
348
|
+
const rightClassification = isRecord(right.item) ? classifyNetworkRequestFailure(right.item) : undefined;
|
|
349
|
+
const leftRank = leftClassification?.impact === "actionable" ? 0 : 1;
|
|
350
|
+
const rightRank = rightClassification?.impact === "actionable" ? 0 : 1;
|
|
351
|
+
return leftRank - rightRank || left.index - right.index;
|
|
352
|
+
});
|
|
353
|
+
const prioritizedRequests = [...failedRequests, ...normalRequests];
|
|
354
|
+
shown.push(...prioritizedRequests.slice(0, DIAGNOSTIC_REQUEST_PREVIEW_LIMIT).flatMap(({ item, index }) => {
|
|
355
|
+
if (!isRecord(item)) return [`${index + 1}. ${stringifyModelFacing(item)}`];
|
|
356
|
+
return formatNetworkRequestLine(item, index);
|
|
357
|
+
}));
|
|
358
|
+
if (requests.length > DIAGNOSTIC_REQUEST_PREVIEW_LIMIT) {
|
|
359
|
+
shown.push(`... (${requests.length - DIAGNOSTIC_REQUEST_PREVIEW_LIMIT} additional requests omitted from preview; failed requests are shown first when present)`);
|
|
360
|
+
}
|
|
361
|
+
return shown.join("\n");
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function formatNetworkRequestText(data: Record<string, unknown>): string | undefined {
|
|
365
|
+
if (!getStringField(data, "url") && !getStringField(data, "requestId") && !getStringField(data, "id")) {
|
|
366
|
+
return undefined;
|
|
367
|
+
}
|
|
368
|
+
return formatNetworkRequestLine(data, 0).join("\n");
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
interface NetworkRequestActionCandidate {
|
|
372
|
+
filter?: string;
|
|
373
|
+
item: Record<string, unknown>;
|
|
374
|
+
kind: "actionable" | "api" | "benign" | "request";
|
|
375
|
+
requestId: string;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function getSafeNetworkActionValue(value: string | undefined): string | undefined {
|
|
379
|
+
if (!value) return undefined;
|
|
380
|
+
const trimmed = value.trim();
|
|
381
|
+
if (trimmed.length === 0 || redactSensitiveText(trimmed) !== trimmed) return undefined;
|
|
382
|
+
return trimmed;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function getNetworkRequestId(item: Record<string, unknown>): string | undefined {
|
|
386
|
+
return getSafeNetworkActionValue(getStringField(item, "requestId") ?? getStringField(item, "id"));
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function isSensitiveNetworkPathSegment(segment: string): boolean {
|
|
390
|
+
const normalized = segment.toLowerCase();
|
|
391
|
+
return normalized === "auth" || NETWORK_FILTER_SENSITIVE_SEGMENT_TERMS.some((term) => normalized.includes(term));
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function pathFilterMayExposeSensitiveSegment(filter: string): boolean {
|
|
395
|
+
const decoded = (() => {
|
|
396
|
+
try {
|
|
397
|
+
return decodeURIComponent(filter);
|
|
398
|
+
} catch {
|
|
399
|
+
return filter;
|
|
400
|
+
}
|
|
401
|
+
})();
|
|
402
|
+
return decoded.split("/").some((segment) => isSensitiveNetworkPathSegment(segment) || NETWORK_FILTER_OPAQUE_SEGMENT_PATTERN.test(segment));
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function getNetworkRequestPathFilter(item: Record<string, unknown>): string | undefined {
|
|
406
|
+
const url = getStringField(item, "url");
|
|
407
|
+
if (!url) return undefined;
|
|
408
|
+
let filter: string | undefined;
|
|
409
|
+
try {
|
|
410
|
+
filter = new URL(url).pathname;
|
|
411
|
+
} catch {
|
|
412
|
+
filter = url.split(/[?#]/, 1)[0];
|
|
413
|
+
}
|
|
414
|
+
filter = filter?.trim();
|
|
415
|
+
if (!filter || filter === "/" || filter.length > NETWORK_FILTER_MAX_CHARS || pathFilterMayExposeSensitiveSegment(filter)) return undefined;
|
|
416
|
+
return getSafeNetworkActionValue(filter);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function isApiLikeNetworkRequest(item: Record<string, unknown>): boolean {
|
|
420
|
+
const method = (getStringField(item, "method") ?? "GET").toUpperCase();
|
|
421
|
+
const resourceType = (getStringField(item, "resourceType") ?? "").toLowerCase();
|
|
422
|
+
const mimeType = (getStringField(item, "mimeType") ?? "").toLowerCase();
|
|
423
|
+
const filter = getNetworkRequestPathFilter(item) ?? "";
|
|
424
|
+
return resourceType === "fetch" || resourceType === "xhr" || mimeType.includes("json") || /\/(?:api|graphql|rpc)(?:\/|$)/i.test(filter) || !["GET", "HEAD"].includes(method);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function getNetworkRequestActionCandidate(item: Record<string, unknown>): NetworkRequestActionCandidate | undefined {
|
|
428
|
+
const requestId = getNetworkRequestId(item);
|
|
429
|
+
if (!requestId) return undefined;
|
|
430
|
+
const classification = classifyNetworkRequestFailure(item);
|
|
431
|
+
const kind: NetworkRequestActionCandidate["kind"] = classification?.impact === "actionable"
|
|
432
|
+
? "actionable"
|
|
433
|
+
: classification?.impact === "benign"
|
|
434
|
+
? "benign"
|
|
435
|
+
: isApiLikeNetworkRequest(item)
|
|
436
|
+
? "api"
|
|
437
|
+
: "request";
|
|
438
|
+
return { filter: getNetworkRequestPathFilter(item), item, kind, requestId };
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function chooseNetworkRequestActionCandidate(candidates: NetworkRequestActionCandidate[]): NetworkRequestActionCandidate | undefined {
|
|
442
|
+
return candidates.find((candidate) => candidate.kind === "actionable")
|
|
443
|
+
?? candidates.find((candidate) => candidate.kind === "api")
|
|
444
|
+
?? candidates.find((candidate) => candidate.kind === "benign")
|
|
445
|
+
?? candidates[0];
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function formatNetworkRequestActionDescriptor(candidate: NetworkRequestActionCandidate): string {
|
|
449
|
+
const method = getStringField(candidate.item, "method") ?? "GET";
|
|
450
|
+
const status = typeof candidate.item.status === "number" ? String(candidate.item.status) : "pending";
|
|
451
|
+
const target = candidate.filter ? ` ${candidate.filter}` : "";
|
|
452
|
+
return `${status} ${method}${target} [${candidate.requestId}]`;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function getNetworkRequestDetailActionId(candidate: NetworkRequestActionCandidate): string {
|
|
456
|
+
if (candidate.kind === "actionable") return "inspect-actionable-network-request";
|
|
457
|
+
if (candidate.kind === "benign") return "inspect-benign-network-request";
|
|
458
|
+
return "inspect-network-request";
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
export function buildNetworkRequestsNextActions(data: unknown, sessionName: string | undefined): AgentBrowserNextAction[] | undefined {
|
|
462
|
+
if (!isRecord(data)) return undefined;
|
|
463
|
+
const requests = getArrayField(data, "requests");
|
|
464
|
+
if (!requests) return undefined;
|
|
465
|
+
const candidates = requests.flatMap((item) => {
|
|
466
|
+
if (!isRecord(item)) return [];
|
|
467
|
+
const candidate = getNetworkRequestActionCandidate(item);
|
|
468
|
+
return candidate ? [candidate] : [];
|
|
469
|
+
});
|
|
470
|
+
const selected = chooseNetworkRequestActionCandidate(candidates);
|
|
471
|
+
if (!selected) return undefined;
|
|
472
|
+
const descriptor = formatNetworkRequestActionDescriptor(selected);
|
|
473
|
+
const actions: AgentBrowserNextAction[] = [
|
|
474
|
+
{
|
|
475
|
+
id: getNetworkRequestDetailActionId(selected),
|
|
476
|
+
params: { args: withOptionalSessionArgs(sessionName, ["network", "request", selected.requestId]) },
|
|
477
|
+
reason: `Inspect full request details for ${descriptor}.`,
|
|
478
|
+
safety: "Read-only network diagnostic; request inspection must not replace the active page/ref context.",
|
|
479
|
+
tool: "agent_browser",
|
|
480
|
+
},
|
|
481
|
+
];
|
|
482
|
+
if (selected.kind === "actionable") {
|
|
483
|
+
actions.push({
|
|
484
|
+
id: "trace-actionable-network-source",
|
|
485
|
+
params: { networkSourceLookup: { requestId: selected.requestId, ...(sessionName ? { session: sessionName } : {}) } },
|
|
486
|
+
reason: `Look for local source candidates related to ${descriptor}.`,
|
|
487
|
+
safety: "Read-only experimental helper; it reports bounded candidates and may miss bundled or dynamic call sites.",
|
|
488
|
+
tool: "agent_browser",
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
if (selected.filter) {
|
|
492
|
+
actions.push({
|
|
493
|
+
id: "filter-network-requests-by-path",
|
|
494
|
+
params: { args: withOptionalSessionArgs(sessionName, ["network", "requests", "--filter", selected.filter]) },
|
|
495
|
+
reason: `List captured requests matching ${selected.filter}.`,
|
|
496
|
+
safety: "Read-only request-list filter; absence from a compact preview is not proof the request did not happen.",
|
|
497
|
+
tool: "agent_browser",
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
actions.push({
|
|
501
|
+
id: "start-network-har-capture",
|
|
502
|
+
params: { args: withOptionalSessionArgs(sessionName, ["network", "har", "start"]) },
|
|
503
|
+
reason: "Start HAR capture before reproducing the network behavior again.",
|
|
504
|
+
safety: "HARs can contain URLs and headers; stop to an explicit path, inspect metadata, and avoid sharing sensitive captures.",
|
|
505
|
+
tool: "agent_browser",
|
|
506
|
+
});
|
|
507
|
+
return actions.slice(0, NETWORK_NEXT_ACTION_LIMIT);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function formatConsoleText(data: Record<string, unknown>): string | undefined {
|
|
511
|
+
const messages = getArrayField(data, "messages");
|
|
512
|
+
if (!messages) return undefined;
|
|
513
|
+
if (messages.length === 0) return "No console messages.";
|
|
514
|
+
const shown = messages.slice(0, DIAGNOSTIC_LOG_PREVIEW_LIMIT).map((item, index) => {
|
|
515
|
+
if (!isRecord(item)) return `${index + 1}. ${stringifyModelFacing(item)}`;
|
|
516
|
+
const type = redactModelFacingText(getStringField(item, "type") ?? "message");
|
|
517
|
+
const text = getStringField(item, "text") ?? stringifyModelFacing(item);
|
|
518
|
+
return `${index + 1}. [${type}] ${firstLine(redactModelFacingText(text).replace(/\s+/g, " ").trim(), 220)}`;
|
|
519
|
+
});
|
|
520
|
+
if (messages.length > shown.length) {
|
|
521
|
+
shown.push(`... (${messages.length - shown.length} additional console messages omitted from preview)`);
|
|
522
|
+
}
|
|
523
|
+
return shown.join("\n");
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function formatErrorsText(data: Record<string, unknown>): string | undefined {
|
|
527
|
+
const errors = getArrayField(data, "errors");
|
|
528
|
+
if (!errors) return undefined;
|
|
529
|
+
if (errors.length === 0) return "No page errors.";
|
|
530
|
+
const shown = errors.slice(0, DIAGNOSTIC_LOG_PREVIEW_LIMIT).map((item, index) => {
|
|
531
|
+
if (!isRecord(item)) return `${index + 1}. ${stringifyModelFacing(item)}`;
|
|
532
|
+
const text = getStringField(item, "text") ?? stringifyModelFacing(item);
|
|
533
|
+
const location = [
|
|
534
|
+
getStringField(item, "url"),
|
|
535
|
+
typeof item.line === "number" ? `line ${item.line}` : undefined,
|
|
536
|
+
typeof item.column === "number" ? `column ${item.column}` : undefined,
|
|
537
|
+
]
|
|
538
|
+
.filter(Boolean)
|
|
539
|
+
.map((item) => redactModelFacingText(String(item)))
|
|
540
|
+
.join(":");
|
|
541
|
+
const safeText = firstLine(redactModelFacingText(text), 220);
|
|
542
|
+
return location ? `${index + 1}. ${safeText} (${location})` : `${index + 1}. ${safeText}`;
|
|
543
|
+
});
|
|
544
|
+
if (errors.length > shown.length) {
|
|
545
|
+
shown.push(`... (${errors.length - shown.length} additional errors omitted from preview)`);
|
|
546
|
+
}
|
|
547
|
+
return shown.join("\n");
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function formatDashboardText(data: Record<string, unknown>): string | undefined {
|
|
551
|
+
const lines: string[] = [];
|
|
552
|
+
if (typeof data.port === "number") lines.push(`Port: ${data.port}`);
|
|
553
|
+
if (typeof data.pid === "number") lines.push(`PID: ${data.pid}`);
|
|
554
|
+
if (typeof data.stopped === "boolean") lines.push(`Stopped: ${data.stopped}`);
|
|
555
|
+
const reason = getStringField(data, "reason");
|
|
556
|
+
if (reason) lines.push(`Reason: ${redactModelFacingText(reason)}`);
|
|
557
|
+
return lines.length > 0 ? lines.join("\n") : undefined;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function formatChatText(data: Record<string, unknown>): string | undefined {
|
|
561
|
+
const response = getStringField(data, "response") ?? getStringField(data, "message") ?? getStringField(data, "text") ?? getStringField(data, "result");
|
|
562
|
+
if (response) return redactModelFacingText(response);
|
|
563
|
+
const model = getStringField(data, "model");
|
|
564
|
+
const provider = getStringField(data, "provider");
|
|
565
|
+
const lines = [model ? `Model: ${redactModelFacingText(model)}` : undefined, provider ? `Provider: ${redactModelFacingText(provider)}` : undefined].filter(Boolean);
|
|
566
|
+
return lines.length > 0 ? lines.join("\n") : undefined;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function formatDoctorText(data: Record<string, unknown>): string | undefined {
|
|
570
|
+
const lines: string[] = [];
|
|
571
|
+
const status = getStringField(data, "status") ?? getStringField(data, "result");
|
|
572
|
+
if (status) lines.push(`Status: ${redactModelFacingText(status)}`);
|
|
573
|
+
for (const key of ["checks", "issues", "problems"] as const) {
|
|
574
|
+
const items = getArrayField(data, key);
|
|
575
|
+
if (items) lines.push(`${key}: ${items.length}`);
|
|
576
|
+
}
|
|
577
|
+
return lines.length > 0 ? lines.join("\n") : undefined;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function formatCookieRecordText(item: Record<string, unknown>, fallbackName: string): string {
|
|
581
|
+
const name = redactModelFacingText(getStringField(item, "name") ?? fallbackName);
|
|
582
|
+
const domain = getStringField(item, "domain");
|
|
583
|
+
const path = getStringField(item, "path");
|
|
584
|
+
const flags = [item.httpOnly === true ? "httpOnly" : undefined, item.secure === true ? "secure" : undefined].filter(Boolean).join(", ");
|
|
585
|
+
const location = [domain, path].filter(Boolean).join("");
|
|
586
|
+
return [name, location ? `(${redactModelFacingText(location)})` : undefined, flags ? `[${flags}]` : undefined].filter(Boolean).join(" ");
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function formatCookiesText(data: Record<string, unknown>): string | undefined {
|
|
590
|
+
const cookies = getArrayField(data, "cookies");
|
|
591
|
+
if (cookies) {
|
|
592
|
+
if (cookies.length === 0) return "No cookies.";
|
|
593
|
+
return cookies
|
|
594
|
+
.map((item, index) => (isRecord(item) ? formatCookieRecordText(item, `(cookie ${index + 1})`) : `${index + 1}. [REDACTED]`))
|
|
595
|
+
.join("\n");
|
|
596
|
+
}
|
|
597
|
+
if (getStringField(data, "name") || getStringField(data, "domain") || getStringField(data, "path") || Object.hasOwn(data, "value")) {
|
|
598
|
+
return formatCookieRecordText(data, "cookie");
|
|
599
|
+
}
|
|
600
|
+
if (data.set === true) return "Cookie set.";
|
|
601
|
+
if (data.cleared === true || data.clear === true) return "Cookies cleared.";
|
|
602
|
+
return undefined;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function formatStorageText(data: Record<string, unknown>): string | undefined {
|
|
606
|
+
const type = getStringField(data, "type") ?? getStringField(data, "storage") ?? "storage";
|
|
607
|
+
const entries = getArrayField(data, "entries") ?? getArrayField(data, "items");
|
|
608
|
+
if (entries) {
|
|
609
|
+
if (entries.length === 0) return `${type}: no entries.`;
|
|
610
|
+
return entries
|
|
611
|
+
.map((item, index) => {
|
|
612
|
+
if (!isRecord(item)) return `${index + 1}. [REDACTED]`;
|
|
613
|
+
const rawKey = getStringField(item, "key") ?? getStringField(item, "name") ?? `(entry ${index + 1})`;
|
|
614
|
+
const key = redactModelFacingText(rawKey);
|
|
615
|
+
return Object.hasOwn(item, "value") ? `${key}: [REDACTED]` : key;
|
|
616
|
+
})
|
|
617
|
+
.join("\n");
|
|
618
|
+
}
|
|
619
|
+
const key = getStringField(data, "key");
|
|
620
|
+
if (key && Object.hasOwn(data, "value")) return `${type} ${redactModelFacingText(key)}: [REDACTED]`;
|
|
621
|
+
if (key && data.set === true) return `${type} set: ${redactModelFacingText(key)}`;
|
|
622
|
+
if (data.cleared === true || data.clear === true) return `${type} cleared.`;
|
|
623
|
+
return undefined;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
function formatDialogText(data: Record<string, unknown>): string | undefined {
|
|
627
|
+
const lines: string[] = [];
|
|
628
|
+
if (typeof data.open === "boolean") lines.push(data.open ? "Dialog open." : "No dialog open.");
|
|
629
|
+
const type = getStringField(data, "type");
|
|
630
|
+
if (type) lines.push(`Type: ${redactModelFacingText(type)}`);
|
|
631
|
+
const message = getStringField(data, "message");
|
|
632
|
+
if (message) lines.push(`Message: ${/(?:auth|authorization|bearer|cookie|pass(?:word)?|secret|session|token)/i.test(message) ? "[REDACTED]" : redactModelFacingText(message)}`);
|
|
633
|
+
if (data.accepted === true) lines.push("Accepted.");
|
|
634
|
+
if (data.dismissed === true) lines.push("Dismissed.");
|
|
635
|
+
return lines.length > 0 ? lines.join("\n") : undefined;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function formatFrameText(data: Record<string, unknown>): string | undefined {
|
|
639
|
+
const frame = getStringField(data, "frame") ?? getStringField(data, "name") ?? getStringField(data, "selector");
|
|
640
|
+
const url = getStringField(data, "url");
|
|
641
|
+
const title = getStringField(data, "title");
|
|
642
|
+
const lines = [frame ? `Frame: ${redactModelFacingText(frame)}` : undefined, title ? `Title: ${redactModelFacingText(title)}` : undefined, url ? `URL: ${redactModelFacingTextIfSensitive(url)}` : undefined].filter(Boolean);
|
|
643
|
+
return lines.length > 0 ? lines.join("\n") : undefined;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
function formatStateText(data: Record<string, unknown>): string | undefined {
|
|
647
|
+
const states = getArrayField(data, "states") ?? getArrayField(data, "files");
|
|
648
|
+
if (states) {
|
|
649
|
+
if (states.length === 0) return "No saved states.";
|
|
650
|
+
return states
|
|
651
|
+
.map((item, index) => {
|
|
652
|
+
if (!isRecord(item)) return `${index + 1}. ${redactModelFacingTextIfSensitive(stringifyModelFacing(item))}`;
|
|
653
|
+
const name = getStringField(item, "name") ?? getStringField(item, "file") ?? getStringField(item, "path") ?? `(state ${index + 1})`;
|
|
654
|
+
const url = getStringField(item, "url");
|
|
655
|
+
return url ? `${index + 1}. ${redactModelFacingText(name)} — ${redactModelFacingTextIfSensitive(url)}` : `${index + 1}. ${redactModelFacingText(name)}`;
|
|
656
|
+
})
|
|
657
|
+
.join("\n");
|
|
658
|
+
}
|
|
659
|
+
if (data.loaded === true) return `State loaded: ${redactModelFacingText(getStringField(data, "path") ?? getStringField(data, "name") ?? "ok")}`;
|
|
660
|
+
if (data.cleared === true || data.clear === true) return "State cleared.";
|
|
661
|
+
return undefined;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function isSensitivePresentationField(key: string): boolean {
|
|
665
|
+
return SENSITIVE_PRESENTATION_FIELD_PATTERN.test(key);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
function redactStructuredPresentationValue(value: unknown): unknown {
|
|
669
|
+
if (typeof value === "string") return redactModelFacingTextIfSensitive(value);
|
|
670
|
+
if (Array.isArray(value)) return value.map((item) => redactStructuredPresentationValue(item));
|
|
671
|
+
if (!isRecord(value)) return value;
|
|
672
|
+
return Object.fromEntries(
|
|
673
|
+
Object.entries(value).map(([key, entryValue]) => [
|
|
674
|
+
key,
|
|
675
|
+
isSensitivePresentationField(key) ? "[REDACTED]" : redactStructuredPresentationValue(entryValue),
|
|
676
|
+
]),
|
|
677
|
+
);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function redactStatefulValues(value: unknown, sensitiveKeys: Set<string>): unknown {
|
|
681
|
+
if (Array.isArray(value)) return value.map((item) => redactStatefulValues(item, sensitiveKeys));
|
|
682
|
+
if (!isRecord(value)) return redactStructuredPresentationValue(value);
|
|
683
|
+
return Object.fromEntries(
|
|
684
|
+
Object.entries(value).map(([key, entryValue]) => [
|
|
685
|
+
key,
|
|
686
|
+
sensitiveKeys.has(key.toLowerCase()) ? "[REDACTED]" : redactStatefulValues(entryValue, sensitiveKeys),
|
|
687
|
+
]),
|
|
688
|
+
);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
export function redactPresentationData(commandInfo: CommandInfo, data: unknown): unknown {
|
|
692
|
+
if (commandInfo.command === "cookies") return redactStatefulValues(data, new Set(["value"]));
|
|
693
|
+
if (commandInfo.command === "storage") return redactStatefulValues(data, new Set(["value"]));
|
|
694
|
+
return redactStructuredPresentationValue(data);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
export function formatDiagnosticText(commandInfo: CommandInfo, data: Record<string, unknown>): string | undefined {
|
|
698
|
+
if (commandInfo.command === "session") return formatSessionText(data);
|
|
699
|
+
if (commandInfo.command === "profiles") {
|
|
700
|
+
const profiles = getArrayField(data, "profiles");
|
|
701
|
+
if (profiles) return formatProfilesText(profiles, "Chrome profiles");
|
|
702
|
+
}
|
|
703
|
+
if (commandInfo.command === "auth") {
|
|
704
|
+
const profiles = getArrayField(data, "profiles");
|
|
705
|
+
if (profiles) return formatProfilesText(profiles, "auth profiles");
|
|
706
|
+
if (commandInfo.subcommand === "show") return formatAuthShowText(data);
|
|
707
|
+
}
|
|
708
|
+
if (commandInfo.command === "cookies") return formatCookiesText(data);
|
|
709
|
+
if (commandInfo.command === "storage") return formatStorageText(data);
|
|
710
|
+
if (commandInfo.command === "dialog") return formatDialogText(data);
|
|
711
|
+
if (commandInfo.command === "frame") return formatFrameText(data);
|
|
712
|
+
if (commandInfo.command === "state") return formatStateText(data);
|
|
713
|
+
if (commandInfo.command === "network" && commandInfo.subcommand === "requests") return formatNetworkRequestsText(data);
|
|
714
|
+
if (commandInfo.command === "network" && commandInfo.subcommand === "request") return formatNetworkRequestText(data);
|
|
715
|
+
if (commandInfo.command === "diff") return stringifyModelFacing(data);
|
|
716
|
+
if (commandInfo.command === "clipboard") {
|
|
717
|
+
const text = getStringField(data, "text") ?? getStringField(data, "value") ?? getStringField(data, "result");
|
|
718
|
+
if (text) return redactModelFacingText(text);
|
|
719
|
+
}
|
|
720
|
+
if (commandInfo.command === "stream") {
|
|
721
|
+
const streamSummary = getStreamSummary(data);
|
|
722
|
+
if (streamSummary) return streamSummary;
|
|
723
|
+
}
|
|
724
|
+
if (commandInfo.command === "chat") return formatChatText(data);
|
|
725
|
+
if (commandInfo.command === "console") return formatConsoleText(data);
|
|
726
|
+
if (commandInfo.command === "errors") return formatErrorsText(data);
|
|
727
|
+
if (commandInfo.command === "dashboard") return formatDashboardText(data);
|
|
728
|
+
if (commandInfo.command === "doctor") return formatDoctorText(data);
|
|
729
|
+
return undefined;
|
|
730
|
+
}
|