pi-agent-browser-native 0.1.6 → 0.2.1

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.
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * Purpose: Build safe, deterministic agent-browser invocations for the pi-agent-browser extension.
3
- * Responsibilities: Validate raw tool arguments, derive implicit session names from the pi session identity, resolve implicit-session timeout/state helpers, detect explicit session usage, and build the effective CLI argument list passed to the upstream agent-browser binary.
3
+ * Responsibilities: Validate raw tool arguments, derive extension-managed session names from the pi session identity, resolve managed-session timeout/state helpers, detect explicit session usage, and build the effective CLI argument list passed to the upstream agent-browser binary.
4
4
  * Scope: Pure runtime-planning helpers only; no subprocess execution or filesystem access lives here.
5
5
  * Usage: Imported by the extension entrypoint and unit tests before spawning the upstream CLI.
6
- * Invariants/Assumptions: The wrapper stays thin, preserves upstream command vocabulary, and only injects `--json` plus an implicit `--session` when appropriate.
6
+ * Invariants/Assumptions: The wrapper stays thin, preserves upstream command vocabulary, and only injects `--json` plus an extension-managed `--session` when appropriate.
7
7
  */
8
8
 
9
9
  import { createHash, randomUUID } from "node:crypto";
@@ -16,13 +16,6 @@ const IMPLICIT_SESSION_IDLE_TIMEOUT_ENV = "PI_AGENT_BROWSER_IMPLICIT_SESSION_IDL
16
16
  const IMPLICIT_SESSION_CLOSE_TIMEOUT_ENV = "PI_AGENT_BROWSER_IMPLICIT_SESSION_CLOSE_TIMEOUT_MS";
17
17
  const DEFAULT_IMPLICIT_SESSION_IDLE_TIMEOUT_MS = 15 * 60 * 1000;
18
18
  const DEFAULT_IMPLICIT_SESSION_CLOSE_TIMEOUT_MS = 5_000;
19
- const INSPECTION_ALLOW_PATTERNS = [
20
- /\bagent[_ -]?browser\s+--(?:help|version)\b/i,
21
- /\bagent[_ -]?browser\b.*\b(?:help|version|docs?|documentation|tool contract|tool guidance|tool description)\b/i,
22
- /\b(?:help|version|docs?|documentation|tool contract|tool guidance|tool description)\b.*\bagent[_ -]?browser\b/i,
23
- /\bdebug(?:ging)?\b.*\b(?:agent[_ -]?browser|agent_browser|browser integration)\b/i,
24
- /\bwhy\s+(?:isn't|is not|doesn't|does not)\b.*\b(?:agent[_ -]?browser|agent_browser)\b/i,
25
- ];
26
19
  const LEGACY_BASH_ALLOW_PATTERNS = [
27
20
  /\b(?:bash-oriented workflow|bash workflow)\b/i,
28
21
  /\b(?:use|via|through|with)\s+bash\b/i,
@@ -56,23 +49,49 @@ const GLOBAL_FLAGS_WITH_VALUES = new Set([
56
49
  ]);
57
50
  const SHELL_OPERATOR_TOKENS = new Set(["&&", "||", "|", ";", ">", ">>", "<"]);
58
51
  const MAX_PROJECT_SLUG_LENGTH = 24;
52
+ const SESSION_NAME_CWD_HASH_LENGTH = 8;
53
+ const SESSION_NAME_SESSION_ID_LENGTH = 12;
59
54
 
60
55
  export interface CommandInfo {
61
56
  command?: string;
62
57
  subcommand?: string;
63
58
  }
64
59
 
60
+ export type SessionMode = "auto" | "fresh";
61
+
62
+ export interface SessionRecoveryHint {
63
+ exampleArgs: string[];
64
+ exampleParams: { args: string[]; sessionMode: "fresh" };
65
+ reason: string;
66
+ recommendedSessionMode: "fresh";
67
+ }
68
+
69
+ export interface InvalidValueFlagDetails {
70
+ flag: string;
71
+ index: number;
72
+ reason: "missing-value" | "unexpected-flag";
73
+ receivedToken?: string;
74
+ }
75
+
65
76
  export interface ExecutionPlan {
66
77
  commandInfo: CommandInfo;
67
78
  effectiveArgs: string[];
79
+ invalidValueFlag?: InvalidValueFlagDetails;
80
+ managedSessionName?: string;
81
+ recoveryHint?: SessionRecoveryHint;
68
82
  sessionName?: string;
69
83
  startupScopedFlags: string[];
70
84
  usedImplicitSession: boolean;
71
85
  validationError?: string;
72
86
  }
73
87
 
88
+ export interface ManagedSessionState {
89
+ active: boolean;
90
+ replacedSessionName?: string;
91
+ sessionName: string;
92
+ }
93
+
74
94
  export interface PromptPolicy {
75
- allowAgentBrowserInspection: boolean;
76
95
  allowLegacyAgentBrowserBash: boolean;
77
96
  }
78
97
 
@@ -103,25 +122,38 @@ export function getImplicitSessionCloseTimeoutMs(env: NodeJS.ProcessEnv = proces
103
122
  return parseTimeoutMs(env[IMPLICIT_SESSION_CLOSE_TIMEOUT_ENV], 0) ?? DEFAULT_IMPLICIT_SESSION_CLOSE_TIMEOUT_MS;
104
123
  }
105
124
 
106
- export function resolveImplicitSessionActiveState(options: {
125
+ export function resolveManagedSessionState(options: {
107
126
  command?: string;
127
+ managedSessionName?: string;
108
128
  priorActive: boolean;
129
+ priorSessionName: string;
109
130
  succeeded: boolean;
110
- usedImplicitSession: boolean;
111
- }): boolean {
112
- const { command, priorActive, succeeded, usedImplicitSession } = options;
113
- if (!usedImplicitSession) return priorActive;
114
- if (command === "close") {
115
- return succeeded ? false : priorActive;
131
+ }): ManagedSessionState {
132
+ const { command, managedSessionName, priorActive, priorSessionName, succeeded } = options;
133
+ if (!managedSessionName) {
134
+ return { active: priorActive, sessionName: priorSessionName };
116
135
  }
117
- if (!command) return priorActive;
118
- return priorActive || succeeded;
136
+ if (command === "close" && managedSessionName === priorSessionName) {
137
+ return { active: succeeded ? false : priorActive, sessionName: priorSessionName };
138
+ }
139
+ if (!succeeded) {
140
+ return { active: priorActive, sessionName: priorSessionName };
141
+ }
142
+ return {
143
+ active: true,
144
+ replacedSessionName: priorActive && priorSessionName !== managedSessionName ? priorSessionName : undefined,
145
+ sessionName: managedSessionName,
146
+ };
119
147
  }
120
148
 
121
149
  export function createEphemeralSessionSeed(): string {
122
150
  return randomUUID();
123
151
  }
124
152
 
153
+ function createCwdHash(cwd: string): string {
154
+ return createHash("sha256").update(`cwd:${cwd}`).digest("hex").slice(0, SESSION_NAME_CWD_HASH_LENGTH);
155
+ }
156
+
125
157
  export function createImplicitSessionName(
126
158
  sessionId: string | undefined,
127
159
  cwd: string,
@@ -133,13 +165,25 @@ export function createImplicitSessionName(
133
165
  .replace(/[^a-z0-9]+/g, "-")
134
166
  .replace(/^-+|-+$/g, "")
135
167
  .slice(0, MAX_PROJECT_SLUG_LENGTH) || "project";
136
- const stableSessionId = sessionId?.replace(/-/g, "").slice(0, 12);
168
+ const cwdHash = createCwdHash(cwd);
169
+ const stableSessionId = sessionId?.replace(/-/g, "").slice(0, SESSION_NAME_SESSION_ID_LENGTH);
137
170
  if (stableSessionId && stableSessionId.length > 0) {
138
- return `piab-${slug}-${stableSessionId}`;
171
+ return `piab-${slug}-${stableSessionId}-${cwdHash}`;
139
172
  }
140
173
 
141
- const digest = createHash("sha256").update(`ephemeral:${cwd}:${ephemeralSeed}`).digest("hex").slice(0, 12);
142
- return `piab-${slug}-${digest}`;
174
+ const digest = createHash("sha256")
175
+ .update(`ephemeral:${cwd}:${ephemeralSeed}`)
176
+ .digest("hex")
177
+ .slice(0, SESSION_NAME_SESSION_ID_LENGTH);
178
+ return `piab-${slug}-${digest}-${cwdHash}`;
179
+ }
180
+
181
+ export function createFreshSessionName(baseSessionName: string, ephemeralSeed: string, ordinal: number): string {
182
+ const suffix = createHash("sha256")
183
+ .update(`fresh:${baseSessionName}:${ephemeralSeed}:${ordinal}`)
184
+ .digest("hex")
185
+ .slice(0, 10);
186
+ return `${baseSessionName}-fresh-${suffix}`;
143
187
  }
144
188
 
145
189
  export function validateToolArgs(args: string[]): string | undefined {
@@ -155,6 +199,54 @@ export function validateToolArgs(args: string[]): string | undefined {
155
199
  return undefined;
156
200
  }
157
201
 
202
+ function getInvalidValueFlagDetails(args: string[]): InvalidValueFlagDetails | undefined {
203
+ for (const [index, token] of args.entries()) {
204
+ if (!token.startsWith("-")) {
205
+ continue;
206
+ }
207
+ const normalizedToken = token.split("=", 1)[0] ?? token;
208
+ if (!GLOBAL_FLAGS_WITH_VALUES.has(normalizedToken)) {
209
+ continue;
210
+ }
211
+ if (token.includes("=")) {
212
+ const value = token.slice(token.indexOf("=") + 1).trim();
213
+ if (value.length === 0) {
214
+ return {
215
+ flag: normalizedToken,
216
+ index,
217
+ reason: "missing-value",
218
+ };
219
+ }
220
+ continue;
221
+ }
222
+ const receivedToken = args[index + 1];
223
+ if (receivedToken === undefined) {
224
+ return {
225
+ flag: normalizedToken,
226
+ index,
227
+ reason: "missing-value",
228
+ };
229
+ }
230
+ if (receivedToken.startsWith("-")) {
231
+ return {
232
+ flag: normalizedToken,
233
+ index,
234
+ reason: "unexpected-flag",
235
+ receivedToken,
236
+ };
237
+ }
238
+ continue;
239
+ }
240
+ return undefined;
241
+ }
242
+
243
+ function formatInvalidValueFlagError(details: InvalidValueFlagDetails): string {
244
+ if (details.reason === "unexpected-flag" && details.receivedToken) {
245
+ return `Flag \`${details.flag}\` requires a value, but received \`${details.receivedToken}\` instead. Pass a non-flag value immediately after \`${details.flag}\`.`;
246
+ }
247
+ return `Flag \`${details.flag}\` requires a value immediately after it. Pass a non-flag token like \`${details.flag} demo\`.`;
248
+ }
249
+
158
250
  function hasFlagToken(args: string[], flag: string): boolean {
159
251
  return args.some((token) => token === flag || token.startsWith(`${flag}=`));
160
252
  }
@@ -177,7 +269,6 @@ export function getStartupScopedFlags(args: string[]): string[] {
177
269
 
178
270
  export function buildPromptPolicy(prompt: string): PromptPolicy {
179
271
  return {
180
- allowAgentBrowserInspection: INSPECTION_ALLOW_PATTERNS.some((pattern) => pattern.test(prompt)),
181
272
  allowLegacyAgentBrowserBash: LEGACY_BASH_ALLOW_PATTERNS.some((pattern) => pattern.test(prompt)),
182
273
  };
183
274
  }
@@ -212,27 +303,60 @@ export function getLatestUserPrompt(branch: unknown[]): string {
212
303
 
213
304
  export function buildExecutionPlan(
214
305
  args: string[],
215
- options: { implicitSessionActive: boolean; implicitSessionName: string; useActiveSession: boolean },
306
+ options: {
307
+ freshSessionName: string;
308
+ managedSessionActive: boolean;
309
+ managedSessionName: string;
310
+ sessionMode: SessionMode;
311
+ },
216
312
  ): ExecutionPlan {
313
+ const effectiveArgs = args.includes("--json") ? [] : ["--json"];
314
+ const invalidValueFlag = getInvalidValueFlagDetails(args);
315
+ if (invalidValueFlag) {
316
+ return {
317
+ commandInfo: {},
318
+ effectiveArgs,
319
+ invalidValueFlag,
320
+ startupScopedFlags: [],
321
+ usedImplicitSession: false,
322
+ validationError: formatInvalidValueFlagError(invalidValueFlag),
323
+ };
324
+ }
325
+
217
326
  const commandInfo = parseCommandInfo(args);
218
327
  const explicitSessionName = extractExplicitSessionName(args);
219
328
  const startupScopedFlags = getStartupScopedFlags(args);
220
- const effectiveArgs = args.includes("--json") ? [] : ["--json"];
329
+ const shouldCreateFreshManagedSession =
330
+ !explicitSessionName && options.sessionMode === "fresh" && commandInfo.command !== undefined && commandInfo.command !== "close";
331
+ let managedSessionName: string | undefined;
332
+ let recoveryHint: SessionRecoveryHint | undefined;
221
333
  let sessionName = explicitSessionName;
222
334
  let usedImplicitSession = false;
223
335
  let validationError: string | undefined;
224
336
 
225
- if (!explicitSessionName && options.useActiveSession) {
226
- if (options.implicitSessionActive && startupScopedFlags.length > 0) {
337
+ if (!explicitSessionName && options.sessionMode === "auto") {
338
+ if (options.managedSessionActive && startupScopedFlags.length > 0) {
339
+ recoveryHint = {
340
+ exampleArgs: args,
341
+ exampleParams: { args, sessionMode: "fresh" },
342
+ reason:
343
+ "Startup-scoped flags like --profile, --session-name, and --cdp need a fresh upstream launch once the extension-managed session is already active.",
344
+ recommendedSessionMode: "fresh",
345
+ };
227
346
  validationError = [
228
- `The current implicit agent-browser session is already running, so startup-scoped flags ${startupScopedFlags.join(", ")} would be ignored by upstream agent-browser.`,
229
- "Reuse the existing implicit session without those flags, or start a fresh upstream session explicitly with `--session ...` (or `useActiveSession: false`) for a new launch.",
347
+ `The current extension-managed agent-browser session is already running, so startup-scoped flags ${startupScopedFlags.join(", ")} would be ignored by upstream agent-browser.`,
348
+ "Retry this call with `sessionMode: \"fresh\"` to force a fresh upstream launch, or pass an explicit `--session ...` if you want to name the new session yourself.",
230
349
  ].join(" ");
231
350
  } else {
232
- effectiveArgs.push("--session", options.implicitSessionName);
233
- sessionName = options.implicitSessionName;
351
+ effectiveArgs.push("--session", options.managedSessionName);
352
+ managedSessionName = options.managedSessionName;
353
+ sessionName = options.managedSessionName;
234
354
  usedImplicitSession = true;
235
355
  }
356
+ } else if (shouldCreateFreshManagedSession) {
357
+ effectiveArgs.push("--session", options.freshSessionName);
358
+ managedSessionName = options.freshSessionName;
359
+ sessionName = options.freshSessionName;
236
360
  }
237
361
 
238
362
  effectiveArgs.push(...args);
@@ -240,6 +364,8 @@ export function buildExecutionPlan(
240
364
  return {
241
365
  commandInfo,
242
366
  effectiveArgs,
367
+ managedSessionName,
368
+ recoveryHint,
243
369
  sessionName,
244
370
  startupScopedFlags,
245
371
  usedImplicitSession,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-agent-browser-native",
3
- "version": "0.1.6",
3
+ "version": "0.2.1",
4
4
  "description": "pi extension that exposes agent-browser as a native tool for browser automation",
5
5
  "type": "module",
6
6
  "author": "Mitch Fultz (https://github.com/fitchmultz)",