pi-agent-browser-native 0.2.34 → 0.2.35

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 (38) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +14 -14
  3. package/docs/ARCHITECTURE.md +19 -13
  4. package/docs/COMMAND_REFERENCE.md +257 -42
  5. package/docs/ELECTRON.md +3 -3
  6. package/docs/RELEASE.md +11 -11
  7. package/docs/REQUIREMENTS.md +5 -5
  8. package/docs/SUPPORT_MATRIX.md +23 -21
  9. package/docs/TOOL_CONTRACT.md +38 -27
  10. package/extensions/agent-browser/index.ts +518 -2402
  11. package/extensions/agent-browser/lib/argv-descriptor.ts +90 -0
  12. package/extensions/agent-browser/lib/argv-grammar.ts +128 -0
  13. package/extensions/agent-browser/lib/command-policy.ts +71 -0
  14. package/extensions/agent-browser/lib/command-taxonomy.ts +336 -0
  15. package/extensions/agent-browser/lib/electron/cleanup.ts +1 -0
  16. package/extensions/agent-browser/lib/executable-path.ts +19 -0
  17. package/extensions/agent-browser/lib/input-modes/params.ts +6 -6
  18. package/extensions/agent-browser/lib/orchestration/batch-stdin.ts +65 -0
  19. package/extensions/agent-browser/lib/orchestration/browser-run/browser-action-model.ts +154 -0
  20. package/extensions/agent-browser/lib/orchestration/browser-run/click-dispatch.ts +149 -0
  21. package/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.ts +10 -28
  22. package/extensions/agent-browser/lib/orchestration/browser-run/final-result.ts +6 -2
  23. package/extensions/agent-browser/lib/orchestration/browser-run/index.ts +33 -27
  24. package/extensions/agent-browser/lib/orchestration/browser-run/prepare.ts +48 -22
  25. package/extensions/agent-browser/lib/orchestration/browser-run/process-output.ts +33 -10
  26. package/extensions/agent-browser/lib/orchestration/browser-run/prompt-guards.ts +93 -0
  27. package/extensions/agent-browser/lib/orchestration/browser-run/session-state.ts +19 -123
  28. package/extensions/agent-browser/lib/orchestration/browser-run/types.ts +26 -1
  29. package/extensions/agent-browser/lib/orchestration/electron-host/index.ts +860 -0
  30. package/extensions/agent-browser/lib/playbook.ts +9 -9
  31. package/extensions/agent-browser/lib/prompt-policy.ts +122 -0
  32. package/extensions/agent-browser/lib/results/action-recommendations.ts +3 -23
  33. package/extensions/agent-browser/lib/results/presentation/navigation.ts +2 -34
  34. package/extensions/agent-browser/lib/runtime.ts +93 -227
  35. package/extensions/agent-browser/lib/session-page-state.ts +31 -14
  36. package/extensions/agent-browser/lib/temp.ts +148 -23
  37. package/package.json +4 -4
  38. package/scripts/agent-browser-capability-baseline.mjs +198 -1
@@ -11,6 +11,23 @@
11
11
  import { createHash, randomUUID } from "node:crypto";
12
12
  import { basename } from "node:path";
13
13
 
14
+ import {
15
+ extractCommandTokens,
16
+ findCommandStartIndex,
17
+ parseArgvDescriptor,
18
+ parseCommandInfo,
19
+ type CommandInfo,
20
+ } from "./argv-descriptor.js";
21
+ import {
22
+ GLOBAL_VALUE_FLAGS_ALLOWING_DASH_VALUE,
23
+ PREVALIDATED_VALUE_FLAGS,
24
+ } from "./argv-grammar.js";
25
+ import { needsManagedSession } from "./command-policy.js";
26
+ import { isCloseCommand, isOpenNavigationCommand } from "./command-taxonomy.js";
27
+
28
+ export type { CommandInfo } from "./argv-descriptor.js";
29
+ export { extractCommandTokens, findCommandStartIndex, parseArgvDescriptor, parseCommandInfo } from "./argv-descriptor.js";
30
+
14
31
  import { isRecord } from "./parsing.js";
15
32
 
16
33
  /**
@@ -81,7 +98,6 @@ const LAUNCH_SCOPED_FLAG_LABEL = LAUNCH_SCOPED_FLAG_DEFINITIONS.map((definition)
81
98
  * tab-correction (the `tab list` + re-select cycle).
82
99
  */
83
100
  const LAUNCH_SCOPED_TAB_CORRECTION_FLAGS = new Set(["--profile", "--session-name", "--state"] as const);
84
- const OPEN_COMMANDS = new Set(["goto", "navigate", "open"]);
85
101
  const OPENAI_HEADLESS_COMPAT_HOSTS = new Set(["chat.com", "chat.openai.com", "chatgpt.com"]);
86
102
  const BRAVE_API_KEY_ENV = "BRAVE_API_KEY";
87
103
  const AGENT_BROWSER_IDLE_TIMEOUT_ENV = "AGENT_BROWSER_IDLE_TIMEOUT_MS";
@@ -89,99 +105,13 @@ const IMPLICIT_SESSION_IDLE_TIMEOUT_ENV = "PI_AGENT_BROWSER_IMPLICIT_SESSION_IDL
89
105
  const IMPLICIT_SESSION_CLOSE_TIMEOUT_ENV = "PI_AGENT_BROWSER_IMPLICIT_SESSION_CLOSE_TIMEOUT_MS";
90
106
  const DEFAULT_IMPLICIT_SESSION_IDLE_TIMEOUT_MS = 15 * 60 * 1000;
91
107
  const DEFAULT_IMPLICIT_SESSION_CLOSE_TIMEOUT_MS = 5_000;
92
- const LEGACY_BASH_ALLOW_PATTERNS = [
93
- /\b(?:bash-oriented workflow|bash workflow)\b/i,
94
- /\b(?:use|via|through|with)\s+bash\b/i,
95
- /\bnpx\s+agent-browser\b/i,
96
- /\bagent-browser\s+--(?:help|version)\b/i,
97
- /\bdebug(?:ging)?\b.*\b(?:agent[_ -]?browser|agent_browser|browser integration)\b/i,
98
- ];
99
- const BROWSER_PROMPT_PATTERNS = [
100
- /\b(?:agent[_ -]?browser|browser automation|eval\s+--stdin|screenshot|snapshot|tab\s+list)\b/i,
101
- /\b(?:react\s+(?:tree|inspect|renders|suspense)|web\s+vitals|core\s+web\s+vitals|pushstate)\b/i,
102
- /\b(?:live\s+docs?|online\s+research|research\s+(?:online|the\s+web)|search\s+(?:online|the\s+web)|web\s+research)\b/i,
103
- /\bbrowser\b.*\b(?:automation|click|fill|navigate|open|page|screenshot|site|snapshot|tab|url|visit|web(?:site| page)?)\b/i,
104
- /\b(?:browse|click|fill|login|navigate|open|visit)\b.*\b(?:https?:\/\/\S+|page|site|tab|url|web(?:site| page)?)\b/i,
105
- ];
106
108
  const INSPECTION_FLAGS = new Set(["--help", "-h", "--version", "-V"]);
107
109
  const SENSITIVE_VALUE_FLAGS = new Set(["--body", "--headers", "--password", "--proxy"]);
108
- const GLOBAL_VALUE_FLAGS_ALLOWING_DASH_VALUE = new Set(["--args"]);
109
- const GLOBAL_BOOLEAN_FLAGS_WITH_OPTIONAL_VALUES = new Set([
110
- "--allow-file-access",
111
- "--annotate",
112
- "--auto-connect",
113
- "--confirm-interactive",
114
- "--content-boundaries",
115
- "--debug",
116
- "--headed",
117
- "--ignore-https-errors",
118
- "--json",
119
- "--no-auto-dialog",
120
- "--quiet",
121
- "-q",
122
- "--verbose",
123
- "-v",
124
- ]);
125
110
  const SENSITIVE_QUERY_PARAM_PATTERN =
126
111
  /^(?:access(?:_|-)?token|api(?:_|-)?key|auth|authorization|bearer|client(?:_|-)?secret|code|cookie|id(?:_|-)?token|key|pass(?:word)?|refresh(?:_|-)?token|secret|sentry(?:_|-)?key|session(?:_|-)?id|sig(?:nature)?|token|write(?:_|-)?key)$/i;
127
112
  const SENSITIVE_FIELD_NAME_PATTERN =
128
113
  /^(?:access(?:_|-)?token|api(?:_|-)?key|auth(?:orization)?|bearer|client(?:_|-)?secret|cookie|id(?:_|-)?token|pass(?:word)?|proxy(?:_|-)?authorization|refresh(?:_|-)?token|secret|sentry(?:_|-)?key|session(?:_|-)?id|set(?:_|-)?cookie|sig(?:nature)?|token|write(?:_|-)?key|x(?:_|-)?api(?:_|-)?key)$/i;
129
114
 
130
- const VALUE_FLAGS = new Set([
131
- "--session",
132
- "--cdp",
133
- "--config",
134
- "--profile",
135
- "--session-name",
136
- "--proxy",
137
- "--proxy-bypass",
138
- "--headers",
139
- "--executable-path",
140
- "--extension",
141
- "--init-script",
142
- "--enable",
143
- "--provider",
144
- "-p",
145
- "--engine",
146
- "--state",
147
- "--download-path",
148
- "--screenshot-dir",
149
- "--screenshot-format",
150
- "--screenshot-quality",
151
- "--color-scheme",
152
- "--device",
153
- "--port",
154
- "--args",
155
- "--user-agent",
156
- "--allowed-domains",
157
- "--action-policy",
158
- "--confirm-actions",
159
- "--max-output",
160
- "--model",
161
- "--baseline",
162
- "--body",
163
- "--categories",
164
- "--curl",
165
- "--depth",
166
- "-d",
167
- "--domain",
168
- "--expires",
169
- "--filter",
170
- "--fn",
171
- "--label",
172
- "--load",
173
- "--name",
174
- "--path",
175
- "--resource-type",
176
- "--sameSite",
177
- "--selector",
178
- "-s",
179
- "--text",
180
- "--timeout",
181
- "--url",
182
- "--username",
183
- "--password",
184
- ]);
185
115
  const DEFAULT_HEADLESS_COMPAT_USER_AGENT_BY_PLATFORM: Partial<Record<NodeJS.Platform, string>> = {
186
116
  darwin: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36",
187
117
  linux: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36",
@@ -194,11 +124,6 @@ const MAX_PROJECT_SLUG_LENGTH = 24;
194
124
  const SESSION_NAME_CWD_HASH_LENGTH = 8;
195
125
  const SESSION_NAME_SESSION_ID_LENGTH = 12;
196
126
 
197
- export interface CommandInfo {
198
- command?: string;
199
- subcommand?: string;
200
- }
201
-
202
127
  export type SessionMode = "auto" | "fresh";
203
128
 
204
129
  export interface SessionRecoveryHint {
@@ -248,13 +173,10 @@ export interface ManagedSessionState {
248
173
  }
249
174
 
250
175
  export interface RestoredManagedSessionState extends ManagedSessionState {
176
+ closedSessionName?: string;
251
177
  freshSessionOrdinal: number;
252
178
  }
253
179
 
254
- export interface PromptPolicy {
255
- allowLegacyAgentBrowserBash: boolean;
256
- }
257
-
258
180
  function isStringArray(value: unknown): value is string[] {
259
181
  return Array.isArray(value) && value.every((item) => typeof item === "string");
260
182
  }
@@ -504,22 +426,10 @@ export function redactInvocationArgs(args: string[]): string[] {
504
426
  return redacted;
505
427
  }
506
428
 
507
- export function shouldAppendBrowserSystemPrompt(prompt: string): boolean {
508
- const normalizedPrompt = prompt.trim();
509
- if (normalizedPrompt.length === 0) {
510
- return false;
511
- }
512
- return BROWSER_PROMPT_PATTERNS.some((pattern) => pattern.test(normalizedPrompt));
513
- }
514
-
515
429
  export function isPlainTextInspectionArgs(args: string[]): boolean {
516
430
  return args.some((token) => INSPECTION_FLAGS.has(token));
517
431
  }
518
432
 
519
- function isStatelessInspectionCommand(commandInfo: CommandInfo): boolean {
520
- return commandInfo.command === "skills" && ["list", "get", "path"].includes(commandInfo.subcommand ?? "");
521
- }
522
-
523
433
  export function hasUsableBraveApiKey(apiKey: string | null | undefined = process.env[BRAVE_API_KEY_ENV]): boolean {
524
434
  return typeof apiKey === "string" && apiKey.trim().length > 0;
525
435
  }
@@ -556,7 +466,7 @@ export function resolveManagedSessionState(options: {
556
466
  if (!managedSessionName) {
557
467
  return { active: priorActive, sessionName: priorSessionName };
558
468
  }
559
- if (command === "close" && managedSessionName === priorSessionName) {
469
+ if (isCloseCommand(command) && managedSessionName === priorSessionName) {
560
470
  return { active: succeeded ? false : priorActive, sessionName: priorSessionName };
561
471
  }
562
472
  if (!succeeded) {
@@ -594,6 +504,29 @@ function getManagedSessionRestoreRank(options: {
594
504
  return nextRank;
595
505
  }
596
506
 
507
+ function getRestorableManagedSessionName(value: unknown, fallbackSessionName: string): string | undefined {
508
+ return typeof value === "string" && isRestorableManagedSessionName(value, fallbackSessionName) ? value : undefined;
509
+ }
510
+
511
+ function getElectronCleanupClosedManagedSessionNames(details: Record<string, unknown>, fallbackSessionName: string): string[] {
512
+ const electron = isRecord(details.electron) ? details.electron : undefined;
513
+ const cleanup = isRecord(electron?.cleanup) ? electron.cleanup : undefined;
514
+ const results = Array.isArray(cleanup?.results) ? cleanup.results : [];
515
+ const closedSessionNames = new Set<string>();
516
+ for (const result of results) {
517
+ if (!isRecord(result) || !Array.isArray(result.steps)) continue;
518
+ const record = isRecord(result.record) ? result.record : undefined;
519
+ for (const step of result.steps) {
520
+ if (!isRecord(step) || step.resource !== "managed-session") continue;
521
+ if (step.state !== "removed" && step.state !== "already-gone") continue;
522
+ const sessionName = getRestorableManagedSessionName(step.sessionName, fallbackSessionName)
523
+ ?? getRestorableManagedSessionName(record?.sessionName, fallbackSessionName);
524
+ if (sessionName) closedSessionNames.add(sessionName);
525
+ }
526
+ }
527
+ return [...closedSessionNames];
528
+ }
529
+
597
530
  export function restoreManagedSessionStateFromBranch(
598
531
  branch: unknown[],
599
532
  fallbackSessionName: string,
@@ -603,9 +536,21 @@ export function restoreManagedSessionStateFromBranch(
603
536
  sessionName: fallbackSessionName,
604
537
  };
605
538
  let activeRestoreRank = 0;
539
+ let closedSessionName: string | undefined;
606
540
  let freshSessionOrdinal = 0;
607
541
  const freshSessionRanks = new Map<string, number>();
608
542
 
543
+ const applyManagedClose = (sessionName: string): void => {
544
+ const restoreRank = getManagedSessionRestoreRank({
545
+ fallbackSessionName,
546
+ freshSessionRanks,
547
+ sessionName,
548
+ });
549
+ if (restoreRank === undefined || sessionName !== restoredState.sessionName) return;
550
+ restoredState = { active: false, sessionName: restoredState.sessionName };
551
+ closedSessionName = sessionName;
552
+ };
553
+
609
554
  for (const entry of branch) {
610
555
  if (!isRecord(entry) || entry.type !== "message") {
611
556
  continue;
@@ -623,17 +568,35 @@ export function restoreManagedSessionStateFromBranch(
623
568
  continue;
624
569
  }
625
570
 
571
+ for (const sessionName of getElectronCleanupClosedManagedSessionNames(details, fallbackSessionName)) {
572
+ applyManagedClose(sessionName);
573
+ }
574
+
626
575
  const explicitSessionName = extractExplicitSessionName(args);
627
576
  const sessionName = typeof details.sessionName === "string" ? details.sessionName : undefined;
628
577
  const sessionMode = details.sessionMode === "fresh" || details.sessionMode === "auto" ? details.sessionMode : undefined;
629
578
  const usedImplicitSession = details.usedImplicitSession === true;
579
+ const command = typeof details.command === "string" ? details.command : parseCommandInfo(args).command;
580
+ const commandClosesSession = isCloseCommand(command);
581
+ const outcome = typeof details.managedSessionOutcome === "object" && details.managedSessionOutcome !== null ? details.managedSessionOutcome as Record<string, unknown> : undefined;
582
+ const outcomeStatus = typeof outcome?.status === "string" ? outcome.status : undefined;
583
+ const outcomeCurrentSessionName = typeof outcome?.currentSessionName === "string" ? outcome.currentSessionName : undefined;
584
+ const outcomeAttemptedSessionName = getRestorableManagedSessionName(outcome?.attemptedSessionName, fallbackSessionName);
585
+ const outcomeClosedSessionName = outcomeStatus === "closed" && outcome?.succeeded === true
586
+ ? outcomeAttemptedSessionName ?? getRestorableManagedSessionName(outcomeCurrentSessionName, fallbackSessionName) ?? getRestorableManagedSessionName(sessionName, fallbackSessionName)
587
+ : undefined;
588
+ const restorableDetailSessionName = getRestorableManagedSessionName(sessionName, fallbackSessionName);
589
+ const explicitCloseSessionName = commandClosesSession && explicitSessionName && restorableDetailSessionName === explicitSessionName
590
+ ? restorableDetailSessionName
591
+ : undefined;
630
592
  const managedSessionName =
631
593
  !explicitSessionName &&
632
- sessionName &&
633
- isRestorableManagedSessionName(sessionName, fallbackSessionName) &&
594
+ restorableDetailSessionName &&
634
595
  (usedImplicitSession || sessionMode === "fresh")
635
- ? sessionName
636
- : undefined;
596
+ ? restorableDetailSessionName
597
+ : commandClosesSession
598
+ ? outcomeClosedSessionName ?? explicitCloseSessionName
599
+ : undefined;
637
600
  if (!managedSessionName) {
638
601
  continue;
639
602
  }
@@ -646,25 +609,19 @@ export function restoreManagedSessionStateFromBranch(
646
609
  if (restoreRank === undefined) {
647
610
  continue;
648
611
  }
612
+ freshSessionOrdinal = Math.max(freshSessionOrdinal, restoreRank);
649
613
 
650
614
  const messageIsError = typeof message.isError === "boolean" ? message.isError : undefined;
651
615
  const exitCode = typeof details.exitCode === "number" ? details.exitCode : undefined;
652
- const outcome = typeof details.managedSessionOutcome === "object" && details.managedSessionOutcome !== null ? details.managedSessionOutcome as Record<string, unknown> : undefined;
653
- const outcomeStatus = typeof outcome?.status === "string" ? outcome.status : undefined;
654
- const outcomeCurrentSessionName = typeof outcome?.currentSessionName === "string" ? outcome.currentSessionName : undefined;
655
616
  const outcomeActiveAfter = outcome?.activeAfter === true;
656
617
  const outcomeRepresentsActiveCurrentSession = outcomeActiveAfter && outcomeCurrentSessionName === managedSessionName && (outcomeStatus === "created" || outcomeStatus === "replaced" || outcomeStatus === "unchanged");
657
618
  const succeeded = outcomeRepresentsActiveCurrentSession ? true : messageIsError === undefined ? exitCode === undefined || exitCode === 0 : !messageIsError;
658
- const command = typeof details.command === "string" ? details.command : parseCommandInfo(args).command;
659
- if ((succeeded || outcomeRepresentsActiveCurrentSession) && sessionMode === "fresh") {
660
- freshSessionOrdinal += 1;
661
- }
662
- const staleCompletion = succeeded && command !== "close" && restoreRank < activeRestoreRank;
663
- if (staleCompletion) {
619
+ if (commandClosesSession) {
620
+ if (succeeded) applyManagedClose(managedSessionName);
664
621
  continue;
665
622
  }
666
- const staleClose = command === "close" && restoredState.active && managedSessionName !== restoredState.sessionName;
667
- if (staleClose) {
623
+ const staleCompletion = succeeded && restoreRank < activeRestoreRank;
624
+ if (staleCompletion) {
668
625
  continue;
669
626
  }
670
627
 
@@ -675,13 +632,15 @@ export function restoreManagedSessionStateFromBranch(
675
632
  priorSessionName: restoredState.sessionName,
676
633
  succeeded,
677
634
  });
678
- if (succeeded && command !== "close" && restoredState.active) {
635
+ if (succeeded && restoredState.active) {
679
636
  activeRestoreRank = restoreRank;
637
+ closedSessionName = undefined;
680
638
  }
681
639
  }
682
640
 
683
641
  return {
684
642
  ...restoredState,
643
+ ...(closedSessionName ? { closedSessionName } : {}),
685
644
  freshSessionOrdinal,
686
645
  };
687
646
  }
@@ -739,11 +698,6 @@ export function validateToolArgs(args: string[]): string | undefined {
739
698
  return undefined;
740
699
  }
741
700
 
742
- function isBooleanLiteral(token: string | undefined): boolean {
743
- const normalized = token?.trim().toLowerCase();
744
- return normalized === "true" || normalized === "false";
745
- }
746
-
747
701
  function getInvalidValueFlagDetails(args: string[]): InvalidValueFlagDetails | undefined {
748
702
  for (let index = 0; index < args.length; index += 1) {
749
703
  const token = args[index];
@@ -751,7 +705,7 @@ function getInvalidValueFlagDetails(args: string[]): InvalidValueFlagDetails | u
751
705
  continue;
752
706
  }
753
707
  const normalizedToken = token.split("=", 1)[0] ?? token;
754
- if (!VALUE_FLAGS.has(normalizedToken)) {
708
+ if (!PREVALIDATED_VALUE_FLAGS.has(normalizedToken)) {
755
709
  continue;
756
710
  }
757
711
  if (token.includes("=")) {
@@ -876,7 +830,7 @@ function getDefaultHeadlessCompatUserAgent(platform: NodeJS.Platform = process.p
876
830
  }
877
831
 
878
832
  function getCompatibilityWorkaround(args: string[], commandInfo: CommandInfo): CompatibilityWorkaround | undefined {
879
- if (!commandInfo.command || !OPEN_COMMANDS.has(commandInfo.command) || !commandInfo.subcommand) {
833
+ if (!commandInfo.command || !isOpenNavigationCommand(commandInfo.command) || !commandInfo.subcommand) {
880
834
  return undefined;
881
835
  }
882
836
  if (hasFlagToken(args, "--user-agent")) {
@@ -947,40 +901,6 @@ export function hasLaunchScopedTabCorrectionFlag(args: string[]): boolean {
947
901
  });
948
902
  }
949
903
 
950
- export function buildPromptPolicy(prompt: string): PromptPolicy {
951
- return {
952
- allowLegacyAgentBrowserBash: LEGACY_BASH_ALLOW_PATTERNS.some((pattern) => pattern.test(prompt)),
953
- };
954
- }
955
-
956
- function getMessageText(content: unknown): string {
957
- if (typeof content === "string") return content;
958
- if (!Array.isArray(content)) return "";
959
-
960
- return content
961
- .map((item) => {
962
- if (typeof item !== "object" || item === null) return "";
963
- return item.type === "text" && typeof item.text === "string" ? item.text : "";
964
- })
965
- .filter((text) => text.length > 0)
966
- .join("\n");
967
- }
968
-
969
- export function getLatestUserPrompt(branch: unknown[]): string {
970
- for (let index = branch.length - 1; index >= 0; index -= 1) {
971
- const entry = branch[index];
972
- if (typeof entry !== "object" || entry === null || !("type" in entry) || entry.type !== "message") {
973
- continue;
974
- }
975
- const message = "message" in entry ? entry.message : undefined;
976
- if (typeof message !== "object" || message === null || !("role" in message) || message.role !== "user") {
977
- continue;
978
- }
979
- return getMessageText("content" in message ? message.content : undefined);
980
- }
981
- return "";
982
- }
983
-
984
904
  export function buildExecutionPlan(
985
905
  args: string[],
986
906
  options: {
@@ -993,8 +913,10 @@ export function buildExecutionPlan(
993
913
  const invalidValueFlag = getInvalidValueFlagDetails(args);
994
914
  const startupScopedFlags = getStartupScopedFlags(args);
995
915
  const plainTextInspection = isPlainTextInspectionArgs(args);
996
- const commandInfo = parseCommandInfo(args);
997
- const statelessInspection = plainTextInspection || isStatelessInspectionCommand(commandInfo);
916
+ const argvDescriptor = parseArgvDescriptor(args);
917
+ const commandTokens = argvDescriptor.commandTokens;
918
+ const commandInfo = argvDescriptor.commandInfo;
919
+ const commandNeedsManagedSession = !plainTextInspection && needsManagedSession(argvDescriptor);
998
920
  const effectiveArgs = plainTextInspection ? [...args] : args.includes("--json") ? [] : ["--json"];
999
921
  if (invalidValueFlag) {
1000
922
  return {
@@ -1020,7 +942,7 @@ export function buildExecutionPlan(
1020
942
 
1021
943
  const explicitSessionName = extractExplicitSessionName(args);
1022
944
  const shouldCreateFreshManagedSession =
1023
- !explicitSessionName && options.sessionMode === "fresh" && commandInfo.command !== undefined && commandInfo.command !== "close";
945
+ !explicitSessionName && options.sessionMode === "fresh" && commandInfo.command !== undefined && !isCloseCommand(commandInfo.command);
1024
946
  const compatibilityWorkaround = getCompatibilityWorkaround(args, commandInfo);
1025
947
  let managedSessionName: string | undefined;
1026
948
  let recoveryHint: SessionRecoveryHint | undefined;
@@ -1028,7 +950,7 @@ export function buildExecutionPlan(
1028
950
  let usedImplicitSession = false;
1029
951
  let validationError: string | undefined;
1030
952
 
1031
- if (!explicitSessionName && options.sessionMode === "auto" && !statelessInspection) {
953
+ if (!explicitSessionName && options.sessionMode === "auto" && commandNeedsManagedSession) {
1032
954
  if (options.managedSessionActive && startupScopedFlags.length > 0) {
1033
955
  recoveryHint = {
1034
956
  exampleArgs: args,
@@ -1047,7 +969,7 @@ export function buildExecutionPlan(
1047
969
  sessionName = options.managedSessionName;
1048
970
  usedImplicitSession = true;
1049
971
  }
1050
- } else if (shouldCreateFreshManagedSession && !statelessInspection) {
972
+ } else if (shouldCreateFreshManagedSession && commandNeedsManagedSession) {
1051
973
  effectiveArgs.push("--session", options.freshSessionName);
1052
974
  managedSessionName = options.freshSessionName;
1053
975
  sessionName = options.freshSessionName;
@@ -1116,59 +1038,3 @@ export function chooseOpenResultTabCorrection(options: {
1116
1038
  }
1117
1039
  : undefined;
1118
1040
  }
1119
-
1120
- function getOpenCommandTarget(commandTokens: string[]): string | undefined {
1121
- for (let index = 1; index < commandTokens.length; index += 1) {
1122
- const token = commandTokens[index];
1123
- if (token === "--init-script" || token === "--enable") {
1124
- index += 1;
1125
- continue;
1126
- }
1127
- if (token.startsWith("--init-script=") || token.startsWith("--enable=")) {
1128
- continue;
1129
- }
1130
- if (token.startsWith("-")) {
1131
- continue;
1132
- }
1133
- return token;
1134
- }
1135
- return undefined;
1136
- }
1137
-
1138
- export function parseCommandInfo(args: string[]): CommandInfo {
1139
- const commandTokens = extractCommandTokens(args);
1140
- const command = commandTokens[0];
1141
- return {
1142
- command,
1143
- subcommand: command && OPEN_COMMANDS.has(command) ? getOpenCommandTarget(commandTokens) : commandTokens[1],
1144
- };
1145
- }
1146
-
1147
- function findCommandStartIndex(args: string[]): number | undefined {
1148
- for (let index = 0; index < args.length; index += 1) {
1149
- const token = args[index];
1150
- if (token.startsWith("--session=")) {
1151
- continue;
1152
- }
1153
- if (token.startsWith("-")) {
1154
- const normalizedToken = token.split("=", 1)[0] ?? token;
1155
- if (VALUE_FLAGS.has(normalizedToken) && !token.includes("=")) {
1156
- index += 1;
1157
- } else if (
1158
- GLOBAL_BOOLEAN_FLAGS_WITH_OPTIONAL_VALUES.has(normalizedToken) &&
1159
- !token.includes("=") &&
1160
- isBooleanLiteral(args[index + 1])
1161
- ) {
1162
- index += 1;
1163
- }
1164
- continue;
1165
- }
1166
- return index;
1167
- }
1168
- return undefined;
1169
- }
1170
-
1171
- export function extractCommandTokens(args: string[]): string[] {
1172
- const commandStartIndex = findCommandStartIndex(args);
1173
- return commandStartIndex === undefined ? [] : args.slice(commandStartIndex);
1174
- }
@@ -6,6 +6,7 @@
6
6
  * Invariants/Assumptions: One tool-call update token must govern all page-state observations from that invocation; stale overlapping updates must not overwrite newer state.
7
7
  */
8
8
 
9
+ import { isCloseCommand, isReadOnlyDiagnosticSessionTargetCommand } from "./command-taxonomy.js";
9
10
  import { isRecord } from "./parsing.js";
10
11
 
11
12
  export interface SessionTabTarget {
@@ -20,6 +21,7 @@ interface OrderedSessionTabTarget {
20
21
 
21
22
  export interface SessionRefSnapshot {
22
23
  refIds: string[];
24
+ refs?: Record<string, { name: string; role: string }>;
23
25
  target?: SessionTabTarget;
24
26
  }
25
27
 
@@ -132,12 +134,6 @@ function extractBatchResultCommand(item: Record<string, unknown>): string[] {
132
134
  return Array.isArray(item.command) ? item.command.filter((token): token is string => typeof token === "string") : [];
133
135
  }
134
136
 
135
- const READ_ONLY_DIAGNOSTIC_SESSION_TARGET_COMMANDS = new Set(["console", "cookies", "errors", "network", "storage"]);
136
-
137
- function isReadOnlyDiagnosticSessionTargetCommand(command: string | undefined, _subcommand: string | undefined): boolean {
138
- return command !== undefined && READ_ONLY_DIAGNOSTIC_SESSION_TARGET_COMMANDS.has(command);
139
- }
140
-
141
137
  export function extractSessionTabTargetFromCommandData(commandTokens: string[], data: unknown): SessionTabTarget | undefined {
142
138
  const [command, subcommand] = commandTokens;
143
139
  return isReadOnlyDiagnosticSessionTargetCommand(command, subcommand) ? undefined : extractSessionTabTargetFromData(data);
@@ -186,7 +182,7 @@ export function deriveSessionTabTarget(options: {
186
182
  previousTarget?: SessionTabTarget;
187
183
  subcommand?: string;
188
184
  }): SessionTabTarget | undefined {
189
- if (options.command === "close") {
185
+ if (isCloseCommand(options.command)) {
190
186
  return undefined;
191
187
  }
192
188
  const commandDataTarget = isReadOnlyDiagnosticSessionTargetCommand(options.command, options.subcommand)
@@ -234,10 +230,21 @@ function getRestoredSessionTabTarget(details: Record<string, unknown>, command:
234
230
  return storedTarget;
235
231
  }
236
232
 
233
+ function extractRefSnapshotRefs(data: unknown): Record<string, { name: string; role: string }> | undefined {
234
+ if (!isRecord(data) || !isRecord(data.refs)) return undefined;
235
+ const refs = Object.fromEntries(Object.entries(data.refs).flatMap(([refId, entry]) => {
236
+ if (!/^e\d+$/.test(refId) || !isRecord(entry) || typeof entry.name !== "string" || typeof entry.role !== "string") return [];
237
+ return [[refId, { name: entry.name, role: entry.role }] as const];
238
+ }));
239
+ return Object.keys(refs).length > 0 ? refs : undefined;
240
+ }
241
+
237
242
  export function extractRefSnapshotFromData(data: unknown): SessionRefSnapshot | undefined {
238
243
  if (!isRecord(data)) return undefined;
244
+ const refs = extractRefSnapshotRefs(data);
239
245
  return {
240
246
  refIds: isRecord(data.refs) ? Object.keys(data.refs).filter((refId) => /^e\d+$/.test(refId)) : [],
247
+ ...(refs ? { refs } : {}),
241
248
  target: extractSessionTabTargetFromData(data),
242
249
  };
243
250
  }
@@ -295,14 +302,24 @@ function getRestoredRefSnapshotInvalidation(details: Record<string, unknown>, co
295
302
  }
296
303
 
297
304
  function getRestoredRefSnapshot(details: Record<string, unknown>): SessionRefSnapshot | undefined {
298
- if (!isRecord(details.refSnapshot) || !Array.isArray(details.refSnapshot.refIds)) return undefined;
299
- const refIds = details.refSnapshot.refIds.filter((refId): refId is string => typeof refId === "string" && /^e\d+$/.test(refId));
305
+ const refSnapshot = isRecord(details.refSnapshot) ? details.refSnapshot : undefined;
306
+ if (!refSnapshot || !Array.isArray(refSnapshot.refIds)) return undefined;
307
+ const refIds = refSnapshot.refIds.filter((refId): refId is string => typeof refId === "string" && /^e\d+$/.test(refId));
308
+ const refRecord = isRecord(refSnapshot.refs) ? refSnapshot.refs : undefined;
309
+ const refEntries = refRecord
310
+ ? Object.fromEntries(refIds.flatMap((refId) => {
311
+ const entry = refRecord[refId];
312
+ if (!isRecord(entry) || typeof entry.name !== "string" || typeof entry.role !== "string") return [];
313
+ return [[refId, { name: entry.name, role: entry.role }] as const];
314
+ }))
315
+ : undefined;
300
316
  return {
301
317
  refIds,
302
- target: isRecord(details.refSnapshot.target)
318
+ ...(refEntries && Object.keys(refEntries).length > 0 ? { refs: refEntries } : {}),
319
+ target: isRecord(refSnapshot.target)
303
320
  ? normalizeSessionTabTarget({
304
- title: typeof details.refSnapshot.target.title === "string" ? details.refSnapshot.target.title : undefined,
305
- url: typeof details.refSnapshot.target.url === "string" ? details.refSnapshot.target.url : undefined,
321
+ title: typeof refSnapshot.target.title === "string" ? refSnapshot.target.title : undefined,
322
+ url: typeof refSnapshot.target.url === "string" ? refSnapshot.target.url : undefined,
306
323
  })
307
324
  : undefined,
308
325
  };
@@ -340,7 +357,7 @@ function shouldApplyRefStateUpdate(options: {
340
357
  }
341
358
 
342
359
  function stripRefSnapshotOrder(snapshot: OrderedSessionRefSnapshot | SessionRefSnapshot | undefined): SessionRefSnapshot | undefined {
343
- return snapshot ? { refIds: snapshot.refIds, target: snapshot.target } : undefined;
360
+ return snapshot ? { refIds: snapshot.refIds, ...(snapshot.refs ? { refs: snapshot.refs } : {}), target: snapshot.target } : undefined;
344
361
  }
345
362
 
346
363
  function stripRefSnapshotInvalidationOrder(invalidation: OrderedSessionRefSnapshotInvalidation | SessionRefSnapshotInvalidation | undefined): SessionRefSnapshotInvalidation | undefined {
@@ -367,7 +384,7 @@ export class SessionPageState {
367
384
  if (!sessionName) continue;
368
385
  const command = typeof details.command === "string" ? details.command : undefined;
369
386
  const subcommand = typeof details.subcommand === "string" ? details.subcommand : undefined;
370
- if (command === "close" && message.isError !== true) {
387
+ if (isCloseCommand(command) && message.isError !== true) {
371
388
  restoredOrder += 1;
372
389
  state.clearSession(sessionName);
373
390
  continue;