pi-agent-browser-native 0.6.16 → 0.6.17
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 +5 -1
- package/README.md +11 -5
- package/dist/extensions/agent-browser/index.js +476 -437
- package/dist/extensions/agent-browser/lib/command-taxonomy.js +8 -4
- package/dist/extensions/agent-browser/lib/execution-cwd.js +64 -0
- package/dist/extensions/agent-browser/lib/input-modes/lookups.js +1 -1
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/artifact-paths.js +27 -20
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.js +1 -1
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +7 -5
- package/dist/extensions/agent-browser/lib/orchestration/operation-paths.js +76 -0
- package/docs/ARCHITECTURE.md +2 -0
- package/docs/COMMAND_REFERENCE.md +2 -2
- package/docs/SUPPORT_MATRIX.md +1 -1
- package/docs/TOOL_CONTRACT.md +9 -3
- package/package.json +4 -4
|
@@ -316,9 +316,9 @@ export function isSessionTabPinningExcludedCommand(command) {
|
|
|
316
316
|
export function isSessionTabPostCommandCorrectionExcludedCommand(command) {
|
|
317
317
|
return hasCommandCapability(command, "excludedFromPostCommandCorrection");
|
|
318
318
|
}
|
|
319
|
-
export function
|
|
319
|
+
export function getRecordCommandOperandIndices(tokens) {
|
|
320
320
|
if (tokens[0] !== "record" || !["start", "restart"].includes(tokens[1] ?? ""))
|
|
321
|
-
return
|
|
321
|
+
return [];
|
|
322
322
|
const operands = [];
|
|
323
323
|
for (let index = 2; index < tokens.length && operands.length < 2; index += 1) {
|
|
324
324
|
// Native validates the range; bare/non-numeric --fps keeps its old literal meaning.
|
|
@@ -329,9 +329,13 @@ export function getRecordCommandOperands(tokens) {
|
|
|
329
329
|
else if (tokens[index] === "--contact-sheet-threshold" && tokens[index + 1] !== undefined)
|
|
330
330
|
index += 1;
|
|
331
331
|
else
|
|
332
|
-
operands.push(
|
|
332
|
+
operands.push(index);
|
|
333
333
|
}
|
|
334
|
-
return operands.length > 0 ?
|
|
334
|
+
return operands.length > 0 ? operands : [2, 3];
|
|
335
|
+
}
|
|
336
|
+
export function getRecordCommandOperands(tokens) {
|
|
337
|
+
const [path, url] = getRecordCommandOperandIndices(tokens);
|
|
338
|
+
return path === undefined ? {} : { path: tokens[path], url: url === undefined ? undefined : tokens[url] };
|
|
335
339
|
}
|
|
336
340
|
/** Starts conservatively invalidate refs because older supported natives replace the page, even on failure. Restarts invalidate only when they have a URL. */
|
|
337
341
|
export function isRecordPageTransitionCommand(tokens) {
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { dirname, isAbsolute, join } from "node:path";
|
|
3
|
+
import { isRecord } from "./parsing.js";
|
|
4
|
+
import { getAgentBrowserSessionIdentityKey } from "./argv-grammar.js";
|
|
5
|
+
function isDirectoryOwner(source) {
|
|
6
|
+
if (/^(?:npm:pi-change-working-dir|git:github\.com\/fitchmultz\/pi-change-working-dir(?:\.git)?)(?:@.+)?$/.test(source.source))
|
|
7
|
+
return true;
|
|
8
|
+
const directories = [source.baseDir, isAbsolute(source.path) ? dirname(source.path) : undefined];
|
|
9
|
+
return directories.some(directory => {
|
|
10
|
+
if (!directory || !isAbsolute(directory))
|
|
11
|
+
return false;
|
|
12
|
+
try {
|
|
13
|
+
return JSON.parse(readFileSync(join(directory, "package.json"), "utf8")).name === "pi-change-working-dir";
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
/** The synchronous owner reply is captured before any browser policy, queue, or child await. */
|
|
21
|
+
export function resolveExecutionCwd(pi, ctx) {
|
|
22
|
+
const request = { sessionManager: ctx.sessionManager };
|
|
23
|
+
pi.events.emit("pi-change-working-dir:resolve-execution-cwd", request);
|
|
24
|
+
const result = request.result;
|
|
25
|
+
if (result !== undefined) {
|
|
26
|
+
const invalid = "pi-change-working-dir returned an invalid execution directory. Update the extension and restart Pi.";
|
|
27
|
+
if (!isRecord(result) || Array.isArray(result))
|
|
28
|
+
throw new Error(invalid);
|
|
29
|
+
if (result.error !== undefined)
|
|
30
|
+
throw new Error(typeof result.error === "string" && result.error.length > 0 ? result.error : invalid);
|
|
31
|
+
if (typeof result.cwd !== "string" || !isAbsolute(result.cwd) || result.cwd.includes("\0"))
|
|
32
|
+
throw new Error(invalid);
|
|
33
|
+
return result.cwd;
|
|
34
|
+
}
|
|
35
|
+
if (pi.getAllTools().some(tool => tool.name === "change_dir" && isDirectoryOwner(tool.sourceInfo))
|
|
36
|
+
|| pi.getCommands().some(command => /^cwd(?::\d+)?$/.test(command.name) && isDirectoryOwner(command.sourceInfo))) {
|
|
37
|
+
throw new Error("Update pi-change-working-dir and restart Pi to use agent_browser with the selected working directory.");
|
|
38
|
+
}
|
|
39
|
+
return ctx.cwd;
|
|
40
|
+
}
|
|
41
|
+
export function getBrowserCwdError(cwd) {
|
|
42
|
+
try {
|
|
43
|
+
if (statSync(cwd).isDirectory())
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
if (!["ENOENT", "ENOTDIR"].includes(error.code ?? ""))
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
return `Browser launch directory is unavailable: ${cwd}. Restore that directory, or explicitly use sessionMode: "fresh" without --session, or --config from the selected execution directory. The existing browser was left untouched.`;
|
|
51
|
+
}
|
|
52
|
+
export function restoreManagedSessionCwd(branch, sessionName, namespace, fallback) {
|
|
53
|
+
const key = getAgentBrowserSessionIdentityKey(sessionName, namespace);
|
|
54
|
+
for (const entry of [...branch].reverse()) {
|
|
55
|
+
if (!isRecord(entry) || entry.type !== "message" || !isRecord(entry.message) || entry.message.toolName !== "agent_browser")
|
|
56
|
+
continue;
|
|
57
|
+
const details = entry.message.details;
|
|
58
|
+
if (!isRecord(details) || typeof details.sessionName !== "string" || typeof details.managedSessionCwd !== "string" || !isAbsolute(details.managedSessionCwd))
|
|
59
|
+
continue;
|
|
60
|
+
if (getAgentBrowserSessionIdentityKey(details.sessionName, typeof details.namespace === "string" ? details.namespace : undefined) === key)
|
|
61
|
+
return details.managedSessionCwd;
|
|
62
|
+
}
|
|
63
|
+
return fallback;
|
|
64
|
+
}
|
|
@@ -232,7 +232,7 @@ export async function analyzeSourceLookupResults(data, compiled, cwd, context) {
|
|
|
232
232
|
const electronContext = status === "no-candidates" ? context?.electronContext : undefined;
|
|
233
233
|
const workspaceRoot = context?.workspaceRoot ?? cwd;
|
|
234
234
|
if (electronContext) {
|
|
235
|
-
limitations.push(`Workspace source scan is limited to the
|
|
235
|
+
limitations.push(`Workspace source scan is limited to the captured execution directory: ${workspaceRoot}.`, "Packaged Electron app code may live inside installed app resources or app.asar outside the workspace; the wrapper does not unpack asar files or scan app bundle resources.");
|
|
236
236
|
}
|
|
237
237
|
return {
|
|
238
238
|
candidates,
|
|
@@ -2,7 +2,7 @@ import { lstatSync, readlinkSync, realpathSync, statSync } from "node:fs";
|
|
|
2
2
|
import { basename, dirname, extname, join, resolve } from "node:path";
|
|
3
3
|
import { foldAgentBrowserFilesystemIdentity } from "../../argv-grammar.js";
|
|
4
4
|
import { parseWaitCommandTokens } from "../../argv-descriptor.js";
|
|
5
|
-
import { getRecordCommandOperands } from "../../command-taxonomy.js";
|
|
5
|
+
import { getRecordCommandOperandIndices, getRecordCommandOperands } from "../../command-taxonomy.js";
|
|
6
6
|
const SCREENSHOT_IMAGE_EXTENSIONS = [".jpeg", ".jpg", ".png", ".webp"];
|
|
7
7
|
function isSingleScreenshotPathToken(token) {
|
|
8
8
|
const explicitlyRelative = token.startsWith("./") || token.startsWith("../");
|
|
@@ -38,20 +38,25 @@ export function getScreenshotPathTokenIndex(commandTokens) {
|
|
|
38
38
|
return undefined;
|
|
39
39
|
}
|
|
40
40
|
const DIFF_SCREENSHOT_VALUE_FLAGS = new Set(["-b", "--baseline", "-o", "--output", "-s", "--selector", "-t", "--threshold"]);
|
|
41
|
-
function
|
|
42
|
-
|
|
41
|
+
export function getDiffFilePathIndices(commandTokens) {
|
|
42
|
+
if (commandTokens[0] !== "diff" || !["snapshot", "screenshot"].includes(commandTokens[1]))
|
|
43
|
+
return {};
|
|
44
|
+
const valueFlags = commandTokens[1] === "screenshot" ? DIFF_SCREENSHOT_VALUE_FLAGS : new Set(["-b", "--baseline", "-s", "--selector", "-d", "--depth"]);
|
|
45
|
+
const paths = {};
|
|
43
46
|
for (let index = 2; index < commandTokens.length; index += 1) {
|
|
44
47
|
const token = commandTokens[index];
|
|
45
|
-
if (!
|
|
48
|
+
if (!valueFlags.has(token))
|
|
46
49
|
continue;
|
|
47
50
|
const value = commandTokens[index + 1];
|
|
48
51
|
if (value === undefined)
|
|
49
|
-
return
|
|
52
|
+
return {};
|
|
50
53
|
if (token === "-o" || token === "--output")
|
|
51
|
-
|
|
54
|
+
paths.output = index + 1;
|
|
55
|
+
if (token === "-b" || token === "--baseline")
|
|
56
|
+
paths.baseline = index + 1;
|
|
52
57
|
index += 1;
|
|
53
58
|
}
|
|
54
|
-
return
|
|
59
|
+
return paths;
|
|
55
60
|
}
|
|
56
61
|
function canonicalizeArtifactPath(absolutePath, platform, seenSymlinks) {
|
|
57
62
|
let cursor = absolutePath;
|
|
@@ -102,28 +107,30 @@ export function getRecordContactSheetDestination(commandTokens) {
|
|
|
102
107
|
const extension = extname(path);
|
|
103
108
|
return extension ? `${path.slice(0, -extension.length)}.contact-sheet.png` : undefined;
|
|
104
109
|
}
|
|
105
|
-
export function
|
|
110
|
+
export function getExplicitArtifactDestinationIndex(commandTokens) {
|
|
106
111
|
const command = commandTokens[0];
|
|
107
112
|
const subcommand = commandTokens[1];
|
|
108
|
-
if (command === "screenshot")
|
|
109
|
-
|
|
110
|
-
return index === undefined ? undefined : commandTokens[index];
|
|
111
|
-
}
|
|
113
|
+
if (command === "screenshot")
|
|
114
|
+
return getScreenshotPathTokenIndex(commandTokens);
|
|
112
115
|
if (command === "download")
|
|
113
|
-
return
|
|
116
|
+
return 2;
|
|
114
117
|
if (command === "pdf")
|
|
115
|
-
return
|
|
118
|
+
return 1;
|
|
116
119
|
if (command === "wait")
|
|
117
|
-
return parseWaitCommandTokens(commandTokens).
|
|
120
|
+
return parseWaitCommandTokens(commandTokens).downloadPathIndex;
|
|
118
121
|
if (command === "state" && subcommand === "save")
|
|
119
|
-
return
|
|
122
|
+
return 2;
|
|
120
123
|
if (command === "diff" && subcommand === "screenshot")
|
|
121
|
-
return
|
|
124
|
+
return getDiffFilePathIndices(commandTokens).output;
|
|
122
125
|
if (command === "network" && subcommand === "har" && commandTokens[2] === "stop")
|
|
123
|
-
return
|
|
126
|
+
return 3;
|
|
124
127
|
if ((command === "trace" || command === "profiler") && subcommand === "stop")
|
|
125
|
-
return
|
|
128
|
+
return 2;
|
|
126
129
|
if (command === "record")
|
|
127
|
-
return
|
|
130
|
+
return getRecordCommandOperandIndices(commandTokens)[0];
|
|
128
131
|
return undefined;
|
|
129
132
|
}
|
|
133
|
+
export function getExplicitArtifactDestination(commandTokens) {
|
|
134
|
+
const index = getExplicitArtifactDestinationIndex(commandTokens);
|
|
135
|
+
return index === undefined ? undefined : commandTokens[index];
|
|
136
|
+
}
|
|
@@ -863,7 +863,7 @@ export async function collectTimeoutPartialProgress(options) {
|
|
|
863
863
|
if ((options.commandTokens[0] === "session" && options.commandTokens[1] === "info") || isBrowserIndependentRead(options.commandTokens, options.stdin))
|
|
864
864
|
return undefined;
|
|
865
865
|
const rawSteps = getTimeoutProgressSteps(options.compiledJob, options.commandTokens, options.stdin);
|
|
866
|
-
const artifacts = await collectTimeoutArtifactEvidence(options.cwd, rawSteps);
|
|
866
|
+
const artifacts = await collectTimeoutArtifactEvidence(options.operationCwd ?? options.cwd, rawSteps);
|
|
867
867
|
const urlData = await runSessionCommandData({ args: ["get", "url"], cwd: options.cwd, namespace: options.namespace, sessionName: options.sessionName });
|
|
868
868
|
const recoveredUrl = extractStringResultField(urlData, "result") ?? extractStringResultField(urlData, "url");
|
|
869
869
|
const titleData = recoveredUrl
|
|
@@ -147,6 +147,7 @@ function applyBatchNetworkRouteState(options) {
|
|
|
147
147
|
}
|
|
148
148
|
export async function processBrowserOutput(input) {
|
|
149
149
|
const { ctx, cwd, electronPostCommandStatusSettleMs, implicitSessionCloseTimeoutMs, sessionPageStateUpdate, signal, state } = input;
|
|
150
|
+
const operationCwd = input.operationCwd ?? cwd;
|
|
150
151
|
const { prepared, processResult } = input;
|
|
151
152
|
const { electronChildProcesses, electronLaunchRecords, sessionPageState, traceOwners } = state;
|
|
152
153
|
let artifactManifest = state.artifactManifest;
|
|
@@ -395,7 +396,7 @@ export async function processBrowserOutput(input) {
|
|
|
395
396
|
let fillVerificationDiagnostic;
|
|
396
397
|
let selectorTextVisibilityDiagnostics = [];
|
|
397
398
|
let electronBroadGetTextScopeDiagnostics = [];
|
|
398
|
-
const timeoutPartialProgress = processResult.timedOut && !recordingStopRecovery && !prepared.readConfirmation ? await collectTimeoutPartialProgress({ commandTokens: prepared.commandTokens, compiledJob: prepared.compiledJob, cwd, namespace: prepared.executionPlan.namespace, sessionName: prepared.executionPlan.sessionName, stdin: prepared.runtimeToolStdin }) : undefined;
|
|
399
|
+
const timeoutPartialProgress = processResult.timedOut && !recordingStopRecovery && !prepared.readConfirmation ? await collectTimeoutPartialProgress({ commandTokens: prepared.commandTokens, compiledJob: prepared.compiledJob, cwd, operationCwd: input.operationCwd, namespace: prepared.executionPlan.namespace, sessionName: prepared.executionPlan.sessionName, stdin: prepared.runtimeToolStdin }) : undefined;
|
|
399
400
|
if (!currentSessionTabTarget && timeoutPartialProgress?.currentPage?.source === "live") {
|
|
400
401
|
currentSessionTabTarget = normalizeSessionTabTarget(timeoutPartialProgress.currentPage);
|
|
401
402
|
}
|
|
@@ -574,7 +575,8 @@ export async function processBrowserOutput(input) {
|
|
|
574
575
|
}
|
|
575
576
|
let managedSessionOutcome = buildManagedSessionOutcome({ activeAfter: managedSessionActive, activeBefore: priorManagedSessionActive, attemptedSessionName: managedCloseSessionName, command: commandClosesSession ? "close" : prepared.executionPlan.commandInfo.command, currentSessionName: managedSessionName, currentSessionNamespace: managedSessionNamespace, previousSessionName: priorManagedSessionName, replacedSessionName: replacedManagedSessionName, replacedSessionNamespace: priorManagedSessionNamespace, sessionMode: prepared.sessionMode, succeeded: managedTransitionSucceeded });
|
|
576
577
|
if (prepared.executionPlan.managedSessionName && managedTransitionSucceeded && managedSessionActive) {
|
|
577
|
-
|
|
578
|
+
if (!priorManagedSessionActive || replacedManagedSessionName)
|
|
579
|
+
managedSessionCwd = cwd;
|
|
578
580
|
managedSessionNamespace = prepared.executionPlan.namespace;
|
|
579
581
|
}
|
|
580
582
|
if (sessionStateKey && succeeded) {
|
|
@@ -681,7 +683,7 @@ export async function processBrowserOutput(input) {
|
|
|
681
683
|
}
|
|
682
684
|
if (plainTextUpgrade && errorText)
|
|
683
685
|
presentationEnvelope = { ...presentationEnvelope, success: false, error: errorText };
|
|
684
|
-
let presentation = plainTextInspection ? { artifacts: undefined, batchFailure: undefined, batchSteps: undefined, content: [{ type: "text", text: inspectionText ?? "" }], data: undefined, fullOutputPath: undefined, fullOutputPaths: undefined, imagePath: undefined, imagePaths: undefined, savedFile: undefined, savedFilePath: undefined, summary: `${prepared.redactedArgs.join(" ")} completed` } : recordingStopRecovery && !recordingStopRecovery.batch ? recordingStopRecovery.presentation : await buildToolPresentation({ args: prepared.redactedProcessArgs, artifactManifest, artifactMaxUpdatedAtMs: Date.now(), artifactMinUpdatedAtMs: input.artifactRunStartedAtMs, artifactRequest: screenshotArtifactRequest, batchArtifactRequests: batchScreenshotArtifactRequests, commandInfo: prepared.executionPlan.commandInfo, compiledSemanticAction: prepared.compiledSemanticAction, cwd, envelope: presentationEnvelope, errorText, namespace: prepared.executionPlan.namespace, networkRouteDiagnostics, networkRoutes: activeNetworkRoutes, persistentArtifactStore, previousRecordingContactSheetPath: sessionStateKey ? state.activeRecordingReservations?.get(sessionStateKey)?.contactSheetPath : undefined, piCleanupOwnership: sessionStateKey && (state.ownedManagedSessions.has(sessionStateKey) || prepared.executionPlan.managedSessionName !== undefined) ? "wrapper-managed" : "caller-owned", sessionName: prepared.executionPlan.sessionName });
|
|
686
|
+
let presentation = plainTextInspection ? { artifacts: undefined, batchFailure: undefined, batchSteps: undefined, content: [{ type: "text", text: inspectionText ?? "" }], data: undefined, fullOutputPath: undefined, fullOutputPaths: undefined, imagePath: undefined, imagePaths: undefined, savedFile: undefined, savedFilePath: undefined, summary: `${prepared.redactedArgs.join(" ")} completed` } : recordingStopRecovery && !recordingStopRecovery.batch ? recordingStopRecovery.presentation : await buildToolPresentation({ args: prepared.redactedProcessArgs, artifactManifest, artifactMaxUpdatedAtMs: Date.now(), artifactMinUpdatedAtMs: input.artifactRunStartedAtMs, artifactRequest: screenshotArtifactRequest, batchArtifactRequests: batchScreenshotArtifactRequests, commandInfo: prepared.executionPlan.commandInfo, compiledSemanticAction: prepared.compiledSemanticAction, cwd: operationCwd, envelope: presentationEnvelope, errorText, namespace: prepared.executionPlan.namespace, networkRouteDiagnostics, networkRoutes: activeNetworkRoutes, persistentArtifactStore, previousRecordingContactSheetPath: sessionStateKey ? state.activeRecordingReservations?.get(sessionStateKey)?.contactSheetPath : undefined, piCleanupOwnership: sessionStateKey && (state.ownedManagedSessions.has(sessionStateKey) || prepared.executionPlan.managedSessionName !== undefined) ? "wrapper-managed" : "caller-owned", sessionName: prepared.executionPlan.sessionName });
|
|
685
687
|
if (recordingStopRecovery)
|
|
686
688
|
presentation = mergeRecordingRecoveryPresentation(presentation, recordingStopRecovery);
|
|
687
689
|
const confirmation = readConfirmationEvent ?? prepared.readConfirmation;
|
|
@@ -745,8 +747,8 @@ export async function processBrowserOutput(input) {
|
|
|
745
747
|
? await collectQaAttachedTarget({ currentTarget: currentSessionTabTarget ?? prepared.priorSessionTabTarget, cwd, namespace: prepared.executionPlan.namespace, sessionName: prepared.executionPlan.sessionName, signal })
|
|
746
748
|
: undefined;
|
|
747
749
|
const sourceLookupElectronContext = prepared.compiledSourceLookup ? getSourceLookupElectronContext({ currentTarget: currentSessionTabTarget, electronLaunchRecords, namespace: prepared.executionPlan.namespace, priorTarget: prepared.priorSessionTabTarget, sessionName: prepared.executionPlan.sessionName }) : undefined;
|
|
748
|
-
const sourceLookup = prepared.compiledSourceLookup ? await analyzeSourceLookupResults(presentationEnvelope?.data, prepared.compiledSourceLookup,
|
|
749
|
-
const networkSourceLookup = prepared.compiledNetworkSourceLookup ? redactNetworkSourceLookupAnalysis(await analyzeNetworkSourceLookupResults(presentationEnvelope?.data, prepared.compiledNetworkSourceLookup,
|
|
750
|
+
const sourceLookup = prepared.compiledSourceLookup ? await analyzeSourceLookupResults(presentationEnvelope?.data, prepared.compiledSourceLookup, operationCwd, { electronContext: sourceLookupElectronContext, workspaceRoot: operationCwd }) : undefined;
|
|
751
|
+
const networkSourceLookup = prepared.compiledNetworkSourceLookup ? redactNetworkSourceLookupAnalysis(await analyzeNetworkSourceLookupResults(presentationEnvelope?.data, prepared.compiledNetworkSourceLookup, operationCwd)) : undefined;
|
|
750
752
|
if (networkSourceLookup && presentation.content[0]?.type === "text")
|
|
751
753
|
presentation.content[0] = { ...presentation.content[0], text: `${networkSourceLookup.summary}\n\n${presentation.content[0].text}` };
|
|
752
754
|
else if (networkSourceLookup)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { isAbsolute, resolve } from "node:path";
|
|
2
|
+
import { projectUpstreamGlobalFlags, scanUpstreamGlobalFlagOccurrences } from "../argv-grammar.js";
|
|
3
|
+
import { getDiffFilePathIndices, getExplicitArtifactDestinationIndex } from "./browser-run/artifact-paths.js";
|
|
4
|
+
import { parseBatchCommandArgument, parseUserBatchStdin } from "./batch-stdin.js";
|
|
5
|
+
/** Bind caller file operands without moving the native process/config or interpreting literal form data. */
|
|
6
|
+
export function resolveOperationPaths(args, stdin, cwd, batchStep = false) {
|
|
7
|
+
let resolved = args;
|
|
8
|
+
const setPath = (index, prefix = "") => {
|
|
9
|
+
const path = index === undefined ? undefined : args[index]?.slice(prefix.length);
|
|
10
|
+
if (index === undefined || !path || isAbsolute(path))
|
|
11
|
+
return;
|
|
12
|
+
if (resolved === args)
|
|
13
|
+
resolved = [...args];
|
|
14
|
+
resolved[index] = prefix + resolve(cwd, path);
|
|
15
|
+
};
|
|
16
|
+
const { tokens, indices } = batchStep ? { tokens: args, indices: args.map((_, index) => index) } : projectUpstreamGlobalFlags(args);
|
|
17
|
+
if (!batchStep) {
|
|
18
|
+
for (const flag of ["--config", "--ca-cert", "--executable-path", "--extension", "--init-script", "--download-path", "--screenshot-dir", "--action-policy", "--state"]) {
|
|
19
|
+
for (const occurrence of scanUpstreamGlobalFlagOccurrences(args, flag)) {
|
|
20
|
+
if (flag === "--state" && tokens[0] === "wait" && occurrence.index > indices[0])
|
|
21
|
+
continue;
|
|
22
|
+
setPath(occurrence.index + 1);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
for (const occurrence of scanUpstreamGlobalFlagOccurrences(args, "--profile")) {
|
|
26
|
+
// Native Chrome profile names are identities, not workspace file paths.
|
|
27
|
+
if (occurrence.value && /[\\/]/.test(occurrence.value) && !occurrence.value.startsWith("~"))
|
|
28
|
+
setPath(occurrence.index + 1);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (tokens[0] === "batch") {
|
|
32
|
+
if (tokens.slice(1).some(token => token !== "--bail")) {
|
|
33
|
+
for (let index = 1; index < tokens.length; index++) {
|
|
34
|
+
if (tokens[index] === "--bail")
|
|
35
|
+
continue;
|
|
36
|
+
const row = parseBatchCommandArgument(tokens[index]).step;
|
|
37
|
+
if (!row)
|
|
38
|
+
continue;
|
|
39
|
+
const bound = resolveOperationPaths(row, undefined, cwd, true).args;
|
|
40
|
+
if (bound === row)
|
|
41
|
+
continue;
|
|
42
|
+
if (resolved === args)
|
|
43
|
+
resolved = [...args];
|
|
44
|
+
resolved[indices[index]] = bound.map(token => `'${token.replaceAll("'", "'\\''")}'`).join(" ");
|
|
45
|
+
}
|
|
46
|
+
return { args: resolved, stdin }; // Native ignores stdin when raw rows exist.
|
|
47
|
+
}
|
|
48
|
+
const rows = parseUserBatchStdin(stdin).steps;
|
|
49
|
+
if (!rows?.length)
|
|
50
|
+
return { args: resolved, stdin };
|
|
51
|
+
const bound = rows.map(row => resolveOperationPaths(row, undefined, cwd, true).args);
|
|
52
|
+
return { args: resolved, stdin: bound.some((row, index) => row !== rows[index]) ? JSON.stringify(bound) : stdin };
|
|
53
|
+
}
|
|
54
|
+
setPath(indices[getExplicitArtifactDestinationIndex(tokens) ?? -1]);
|
|
55
|
+
if (tokens[0] === "upload")
|
|
56
|
+
for (let index = 2; index < tokens.length; index++)
|
|
57
|
+
setPath(indices[index]);
|
|
58
|
+
if (tokens[0] === "state" && tokens[1] === "load")
|
|
59
|
+
setPath(indices[2]);
|
|
60
|
+
setPath(indices[getDiffFilePathIndices(tokens).baseline ?? -1]);
|
|
61
|
+
if (tokens[0] === "cookies" && tokens[1] === "set") {
|
|
62
|
+
const index = tokens.indexOf("--curl");
|
|
63
|
+
if (index >= 0)
|
|
64
|
+
setPath(indices[index + 1]);
|
|
65
|
+
}
|
|
66
|
+
if (tokens[0] === "webmcp" && tokens[1] === "invoke") {
|
|
67
|
+
for (let index = 3; index < tokens.length; index++) {
|
|
68
|
+
if (!["--params", "--frame", "--timeout"].includes(tokens[index]))
|
|
69
|
+
continue;
|
|
70
|
+
if (tokens[index] === "--params" && tokens[index + 1]?.startsWith("@"))
|
|
71
|
+
setPath(indices[index + 1], "@");
|
|
72
|
+
index++;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return { args: resolved, stdin };
|
|
76
|
+
}
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -140,6 +140,8 @@ The default extension factory optionally accepts `{ beforeExecute }`. `index.ts`
|
|
|
140
140
|
|
|
141
141
|
## Session model
|
|
142
142
|
|
|
143
|
+
The optional `pi-change-working-dir` event protocol supplies one invocation execution cwd for file operands and workspace scans. It never mutates native `ctx.cwd`. Native config/CLI cwd stays at the project or managed launch anchor; explicit fresh/config choices use the invocation root. Managed launch roots persist in `details.managedSessionCwd`; selection runs inside the existing managed queue so an earlier fresh launch determines the next browser target without changing admitted file paths. Script children retain the outer snapshot. Existing recordings and cached artifact stores keep their original ownership. If a launch root is deleted, validation reports that boundary rather than spawning under another project's config. Public tool/command source manifests identify legacy owners; no private directory journal or process-global cwd is involved.
|
|
144
|
+
|
|
143
145
|
### Default
|
|
144
146
|
|
|
145
147
|
An explicit native `session` default (user/project JSON or `AGENT_BROWSER_SESSION`) selects a caller-owned browser across Pi sessions. Per-call session/namespace flags win. `lib/orchestration/native-session-defaults.ts` reads session/namespace and contributing launch defaults from native config paths and uses sessionless native `--config <path> --json session` to validate each contributing file and resolve its session. Native config continues to own launch/profile/storage settings; the adapter does not duplicate its schema. Explicit config and per-call idle settings are scoped to helpers through `lib/process-environment.ts`. No startup probe, persistent config cache, or new lifecycle service is added.
|
|
@@ -524,7 +524,7 @@ For local app debugging, top-level `sourceLookup` can gather candidate component
|
|
|
524
524
|
{ "sourceLookup": { "selector": "#save", "reactFiberId": "2", "componentName": "SaveButton" } }
|
|
525
525
|
```
|
|
526
526
|
|
|
527
|
-
Top-level `networkSourceLookup` does the same for failed browser requests. When `requestId` is set it adds `network request <requestId>`; when `filter` or `url` is set it also adds `network requests --filter …`, using `url` as the filter pattern when `filter` is omitted. Add `namespace` / `session` when the generated batch should target an explicit upstream namespace/session; `namespace: ""` explicitly selects the default namespace and overrides an ambient namespace. With `requestId` only, the compiled batch is just that request step; failed-request detection still walks the returned batch JSON and treats HTTP status ≥ 400, `failed: true`, or an `error` field as failure. When `filter` or `url` is present, the same heuristics apply but requests are correlated only if their URL matches that substring (either direction). Workspace URL literal search under the
|
|
527
|
+
Top-level `networkSourceLookup` does the same for failed browser requests. When `requestId` is set it adds `network request <requestId>`; when `filter` or `url` is set it also adds `network requests --filter …`, using `url` as the filter pattern when `filter` is omitted. Add `namespace` / `session` when the generated batch should target an explicit upstream namespace/session; `namespace: ""` explicitly selects the default namespace and overrides an ambient namespace. With `requestId` only, the compiled batch is just that request step; failed-request detection still walks the returned batch JSON and treats HTTP status ≥ 400, `failed: true`, or an `error` field as failure. When `filter` or `url` is present, the same heuristics apply but requests are correlated only if their URL matches that substring (either direction). Workspace URL literal search under the captured execution cwd reuses the `sourceLookup` scan rules (`maxWorkspaceFiles` defaults to 2000, hard cap 5000, at most ten `workspace-search` rows, up to eight URL/path needles from the query plus failed request URLs). It reports `details.networkSourceLookup.status` as `failed-requests-found`, `no-failed-requests`, or `no-candidates` and never assigns definitive blame. Request-detail URLs are diagnostic evidence, not active-tab evidence: standalone `network request …` and generated `networkSourceLookup` batches preserve the previous app page target and latest same-page `refSnapshot`.
|
|
528
528
|
|
|
529
529
|
```json
|
|
530
530
|
{ "networkSourceLookup": { "requestId": "req-1", "url": "/api/fail" } }
|
|
@@ -581,7 +581,7 @@ Wrapper result rendering is metadata-first for saved files. Image MIME types com
|
|
|
581
581
|
|
|
582
582
|
`diff screenshot` follows the file-artifact path above for the **diff** image: model-visible text and `details.artifacts` focus on that output, while baseline paths stay out of the artifact summary block, and Pi does **not** auto-inline the diff the way it inlines trusted `screenshot` captures. `state load` may print the loaded path in prose but does not add a saved-file artifact entry the way `state save` does.
|
|
583
583
|
|
|
584
|
-
For screenshot paths under dot-directories such as `.dogfood/run/foo.png`, the wrapper normalizes the requested path to an absolute path before invoking upstream `agent-browser`, verifies the requested file exists, and repairs from an upstream temp screenshot when possible. For direct artifact commands and batch artifact steps (`download`, `pdf`, `screenshot`, `state save`, and `wait --download`), the wrapper creates missing parent directories before launch. A parent-directory failure returns `validation-error` with the attempted directory and `verify-artifact-path` guidance before the browser command runs.
|
|
584
|
+
For screenshot paths under dot-directories such as `.dogfood/run/foo.png`, the wrapper normalizes the requested path to an absolute path before invoking upstream `agent-browser`, verifies the requested file exists, and repairs from an upstream temp screenshot when possible. For direct artifact commands and batch artifact steps (`download`, `pdf`, `screenshot`, `state save`, and `wait --download`), the wrapper creates missing parent directories before launch. A parent-directory failure returns `validation-error` with the attempted directory and `verify-artifact-path` guidance before the browser command runs. Prefer **absolute paths in raw batch artifact rows** because the daemon's working directory may differ from Pi's. When `pi-change-working-dir` selects a different execution root, the wrapper binds relative file operands in direct, stdin, and raw batch rows to that invocation's root; upstream's raw-argument precedence over stdin is unchanged. Otherwise raw strings stay unchanged and screenshot path normalization applies only to direct calls and stdin rows. See [working-directory changes](../README.md#working-directory-changes). Known caller paths appear as `Requested path`; `Absolute path` is the resolved location checked on disk, and `Reported path` exposes a differing screenshot report (including a canonical `/private/tmp` alias) through the existing `tempPath` metadata. No extra canonicalization rewrites upstream arguments.
|
|
585
585
|
|
|
586
586
|
For annotated screenshots in `batch`, put `--annotate` in top-level args instead of inside the screenshot step:
|
|
587
587
|
|
package/docs/SUPPORT_MATRIX.md
CHANGED
|
@@ -129,7 +129,7 @@ Re-run the gates below before each release. Dated outcomes in this table are his
|
|
|
129
129
|
| Configured-source lifecycle | `npm run verify -- lifecycle` (`scripts/verify-lifecycle.mjs`) drives `/reload`, closes and relaunches Pi with the same exact `--session-id`, checks the JSONL session header id, session continuity, slash-command sentinel tokens (`v1` before reload and `v2` after full relaunch because compiled JS package modules are process-cached), persisted spill reachability, and real Pi `tool_result` failure-patch semantics for a QA reclassification with a fake upstream on `PATH`. Default Pi model is `zai/glm-5.2`; default per-step wait is **180000 ms** (`DEFAULT_TIMEOUT_MS`); override model with `--model <id>` and waits with `--timeout-ms <ms>`. Passthrough flags in [`scripts/project.mjs`](https://github.com/fitchmultz/pi-agent-browser-native/blob/main/scripts/project.mjs): `--keep-artifacts`, `--model`, `--verbose`, and `--timeout-ms` plus a value (for example `npm run verify -- lifecycle --model openai-codex/gpt-5.5:minimal --keep-artifacts --verbose --timeout-ms 600000`). | **Pi 0.84.4 on 2026-09-01:** pass; `/reload`, exact-session relaunch, managed continuity, persisted spill reachability, and Pi failure-patch behavior all verified. |
|
|
130
130
|
| Quick isolated Pi smoke | `pi --approve --no-extensions --no-skills -e .` from trusted repo root; checkout-native browser tools only. | **Earlier 0.36.0 / package 0.6.2 / Pi 0.84.4:** isolated checkout tmux smoke passed on 2026-09-01; native `agent_browser` completed version, localhost open, WebMCP list/invoke, visible-text verification, and close. |
|
|
131
131
|
|
|
132
|
-
Runtime floor note: package metadata keeps optional Pi core package peer ranges wildcard per installed Pi package docs, pins direct Pi validation dependencies to 0.
|
|
132
|
+
Runtime floor note: package metadata keeps optional Pi core package peer ranges wildcard per installed Pi package docs, pins direct Pi validation dependencies to 0.87.0, and makes `pi --version` below 0.84.0 a setup failure through `pi-agent-browser-doctor` / `npm run doctor`. The 0.3.0 support-floor change intentionally includes no compatibility shims for older Pi releases.
|
|
133
133
|
|
|
134
134
|
## Baseline checklist by inventory section
|
|
135
135
|
|
package/docs/TOOL_CONTRACT.md
CHANGED
|
@@ -288,6 +288,12 @@ Examples:
|
|
|
288
288
|
{ "args": ["quit"] }
|
|
289
289
|
```
|
|
290
290
|
|
|
291
|
+
### Execution directory
|
|
292
|
+
|
|
293
|
+
Relative file operands, `outputPath`, and source/network-source scans use one `pi-change-working-dir` owner reply captured before browser policy and queues; script children inherit it. The integration uses the synchronous `pi-change-working-dir:resolve-execution-cwd` event with `{ sessionManager, result?: { cwd, error? } }`. Owner errors or invalid replies fail validation. No owner means native `ctx.cwd`; a known older owner identified through public tool/command package provenance requires an update and Pi restart. No private directory journal is read.
|
|
294
|
+
|
|
295
|
+
Browser/session/config ownership stays separate. Ordinary calls retain the native project or managed launch root, while an unnamed fresh launch and explicit native config resolve against the execution directory. `details.managedSessionCwd`, when present, records the managed launch root for transcript replay. Pi package config/trust and artifact-store identity remain unchanged. A deleted launch directory returns `validation-error` before dispatch; restore it or explicitly choose fresh/config from the new directory. See [working-directory changes](../README.md#working-directory-changes).
|
|
296
|
+
|
|
291
297
|
### `script`
|
|
292
298
|
|
|
293
299
|
- type: string, maximum 65,536 characters at the public schema and 65,536 UTF-8 bytes at execution
|
|
@@ -647,7 +653,7 @@ Use raw `args` for direct upstream React inspection when you already know the ex
|
|
|
647
653
|
- **EXPERIMENTAL — candidates only:** failed-request source-hint helper; it reports failed network requests and candidate source hints with evidence instead of assigning blame or proving root cause
|
|
648
654
|
- compiles to existing upstream `batch` commands only: `network request <requestId>` when provided plus `network requests` with `--filter <filter-or-url>` when a filter or URL is provided (if both are set, `filter` wins; when only `url` is set, it becomes the `--filter` argument); optional `namespace` / `session` prepends `--namespace <name>` / `--session <name>` before that generated `batch`; exact `namespace: ""` selects the default namespace and remains explicit so an ambient namespace cannot redirect the lookup
|
|
649
655
|
- detects failed requests from `status >= 400`, `failed: true`, or an `error` field
|
|
650
|
-
- candidate sources come from source-like initiator/stack metadata in upstream network results and bounded local workspace search for URL/path literals under the
|
|
656
|
+
- candidate sources come from source-like initiator/stack metadata in upstream network results and bounded local workspace search for URL/path literals under the captured execution cwd
|
|
651
657
|
- optional `maxWorkspaceFiles` defaults to 2000 and cannot exceed 5000; workspace-search candidates are capped at ten
|
|
652
658
|
- reports `details.compiledNetworkSourceLookup` with the generated batch plan and `details.networkSourceLookup` with `{ status, failedRequests, candidates, limitations, summary }`
|
|
653
659
|
- `details.networkSourceLookup.status` is one of `failed-requests-found`, `no-failed-requests`, or `no-candidates`
|
|
@@ -690,7 +696,7 @@ For `eval --stdin`, put the script in the top-level `stdin` field. The wrapper n
|
|
|
690
696
|
|
|
691
697
|
- type: `string`
|
|
692
698
|
- optional; can be used with successful browser results, most often `eval --stdin`, `get text`, `get html`, `snapshot`, or diagnostic captures. Recording results also export on failure, pending finalization, timeout and recovery; unrelated failed extractions remain unwritten.
|
|
693
|
-
- workspace-relative paths resolve against the
|
|
699
|
+
- workspace-relative paths resolve against the captured execution cwd; absolute paths are used as-is; a leading `@` is stripped for consistency with Pi file arguments
|
|
694
700
|
- after the upstream command completes, the wrapper writes `details.data` when present, otherwise the model-facing text content; objects/arrays are written as pretty JSON with a trailing newline and strings are written as-is. If a direct `details.data` value, a `batch` row's `result`, or the whole batch is compacted, the wrapper instead reads and serializes each full command-redacted pre-compaction payload only from the corresponding live `spill` entry in its own `details.artifactManifest` (`persistent-session` or `process-temp`). It never writes a compact metadata object as a substitute; any missing, evicted, malformed, or untrusted required spill makes the result fail and leaves `outputPath` unwritten.
|
|
695
701
|
- recording exports use `source: "recording-receipt"` and a JSON envelope containing `success`, `error`, `command`/`subcommand`, native session/namespace, `attempt`, `data`, `artifacts`, `artifactVerification`, and optional `recordingRecovery`. A recovered result may have `success: true` while `attempt.success: false` preserves the timeout/error; saving the receipt file never proves the video succeeded. Preflight failures can export an empty receipt with their failed attempt, without inventing an artifact.
|
|
696
702
|
- `outputPath` must not resolve to the same file as a screenshot, download, recording, or other browser artifact produced by that result, including dangling/existing symlink, hardlink, Unicode-fold, and platform-case aliases. When both destinations are known before launch, preflight rejects the call as `validation-error` without browser activity; if an alias becomes apparent only from the completed result, the writer preserves the browser artifact, rejects the result-data write, and reports `details.outputFile.status: "failed"`
|
|
@@ -955,7 +961,7 @@ Additional structured fields can appear when relevant:
|
|
|
955
961
|
- `managedSessionHeadedAutosaveDisabled: true` is the narrower compatibility marker that the targeted session uses the wrapper's default interval `0`, rather than an explicit caller interval. It accompanies `managedSessionHeadedAutosaveInterval: "0"` on active rows and remains omitted for explicitly configured autosave.
|
|
956
962
|
- `managedSessionOutcome` after a managed-session plan reaches process execution (`buildManagedSessionOutcome` / `formatManagedSessionOutcomeText` in `extensions/agent-browser/lib/orchestration/browser-run/session-state.ts`). Populated when `buildExecutionPlan` injects an extension-managed implicit or fresh `--session`, and also when a successful explicit `--session <current-wrapper-managed-session> close` closes the current managed session. It remains omitted for unrelated explicit user-managed sessions and for sessionless inspection/local paths that skip injection. Successful nested-batch lifecycle rows are evaluated in order: a terminal close reports and replays `status: "closed"` even when aggregate artifact verification makes the tool result fail; a later lifecycle-proven browser launch (including a post-close `record stop`) keeps the session active, an explicitly non-launching diagnostic leaves it closed, and an unknown row stays conservatively active even when the failed batch was the first managed call. Fields: `status` (`created`, `replaced`, `unchanged`, `closed`, `preserved`, or `abandoned`), `sessionMode`, `attemptedSessionName`, `previousSessionName`, `currentSessionName`, optional `currentSessionNamespace`, optional `replacedSessionName`, optional `replacedSessionNamespace`, optional `replacedSessionClosed` (false means automatic close failed and the previous session remains wrapper-owned/restorable for explicit cleanup), `activeBefore`, `activeAfter`, `succeeded`, and `summary` (machine-oriented; may include generated session names). Use `currentSessionNamespace` with `currentSessionName` when following preserved-session recovery actions; retry-fresh actions stay in the attempted namespace. Model-visible echo: when `sessionMode` is `"fresh"` **and** `succeeded` is false, or when `replacedSessionClosed` is false after a replacement, the wrapper appends action-oriented `Managed session outcome` and `Recovery` lines without repeating generated session ids in visible prose; session names remain in `details.managedSessionOutcome`. Failed fresh launches may also append `details.nextActions` such as `run-agent-browser-doctor`, `verify-current-managed-session`, `snapshot-current-managed-session`, or `retry-fresh-managed-session`. When other trailing diagnostic prose is also emitted in the same result, that block is concatenated **after** semantic-action candidate lines, overlay/selector-visibility tails, eval hints/warnings, and `Timeout partial progress` (see `rawAppendedDiagnosticText` in `extensions/agent-browser/lib/orchestration/browser-run/final-result.ts`). For `"auto"` failures the same struct may appear on `details` without that extra line. When post-upstream analysis (for example **`qa`** preset failure) flips the overall tool result after a successful batch, or a fresh `job`/batch opens the requested page and then a later step fails, the managed-session transition still reflects that the fresh browser became current. The visible recovery says the fresh launch became current and points to `failureCategory` / `qaPreset` / `batchFailure` for the post-launch failure instead of telling the agent that the old session was preserved.
|
|
957
963
|
- `imagePath` / `imagePaths` for Pi inline image attachments from the **`screenshot`** command (including batched screenshot steps). **`diff screenshot`** still records the diff output as an `image`-kind entry in `details.artifacts`, but it does **not** populate `imagePath` / `imagePaths` or attach an inline image: only plain `screenshot` is treated as a trusted live-capture path for automatic inlining (`isTrustedScreenshotOutput` in `extensions/agent-browser/lib/results/presentation/artifacts.ts`).
|
|
958
|
-
- `artifacts` for saved files such as screenshots, `state save` outputs, `diff screenshot` diff images, PDFs, downloads, `wait --download` / `wait -d` files, traces, CPU profiles, completed video recordings, path-bearing HAR captures, and future recording output paths reported by `record start` / `record restart`. Non-file URL payloads such as `data:` / `blob:` / `http(s):` values are not treated as verified local artifacts. For direct artifact commands and batch artifact steps, the wrapper creates parent directories for requested paths before spawning upstream. Filesystem `mkdir` failures at this shared preparation boundary return `validation-error`, `agentBrowserStarted: false`, the attempted directory and `verify-artifact-path` guidance.
|
|
964
|
+
- `artifacts` for saved files such as screenshots, `state save` outputs, `diff screenshot` diff images, PDFs, downloads, `wait --download` / `wait -d` files, traces, CPU profiles, completed video recordings, path-bearing HAR captures, and future recording output paths reported by `record start` / `record restart`. Non-file URL payloads such as `data:` / `blob:` / `http(s):` values are not treated as verified local artifacts. For direct artifact commands and batch artifact steps, the wrapper creates parent directories for requested paths before spawning upstream. Filesystem `mkdir` failures at this shared preparation boundary return `validation-error`, `agentBrowserStarted: false`, the attempted directory and `verify-artifact-path` guidance. When execution cwd differs from the launch/project root, relative file operands in raw batch strings are bound to the captured invocation root; otherwise raw strings stay unchanged and absolute artifact paths are preferred. Each artifact includes the original saved or requested `path`, resolved `absolutePath`, `kind`/`artifactType`, optional `mediaType`, optional `extension`, best-effort disk metadata such as `exists`, `sizeBytes`, and `updatedAtMs`, plus `requestedPath`, `status`, `cwd`, `session`, `namespace`, and `tempPath` when applicable. `requestedPath` is retained only when known from the caller, separately from reported/resolved locations; a differing screenshot report remains in `tempPath` and is displayed as `Reported path`, whether it is a temporary file or a canonical path alias. Ordinary file `mediaType` values come from bounded PNG/JPEG/GIF/WebP header recognition, not suffixes; unknown, missing, unreadable or truncated headers leave it undefined. Header recognition is not full-file format validation. Inline screenshot attachments use the same byte classifier and existing size limit. For commands that create/update artifacts, a path that existed but was not updated during this command uses `status: "stale"`; observational `wait --download` may accept a file completed just before the wait began. Pending `record start` / `record restart` artifacts use `status: "pending"`, omit `exists` rather than reporting false, and include `recordingState: "openRecording"` / `willExistOnStop: true`. Within one Pi extension process, the wrapper keeps an unbounded transcript-backed active-recording reservation index separate from the bounded artifact manifest, keyed by canonical namespace plus session; still-live process-owned reservations survive branch switches, while known closures are appended after tree navigation and during shutdown/reload so a close on one branch cannot be resurrected after returning to an older branch. Persisted active reservations require absolute storage paths and cwd; their display paths may remain relative. If a journal append fails, the next serialized browser boundary, tree navigation, or shutdown retries all current reservations and known closures. `recordingPersistenceWarning` and visible warning text remain present while restart protection is not durable; successful recovery is quiet and cleanup still runs. Artifact lifecycle calls, explicit `wait --download <path>` / `wait -d <path>` destinations, and result `outputPath` writes serialize around the global destination check/update, every successful direct, ordered nested-batch, fresh-replacement, script, Electron, or shutdown close retires only its exact identity at that lifecycle point, and destination reuse is rejected through lexical, existing or dangling symlink, hardlink, full Unicode-fold, or macOS/Windows case aliases. Batch preflight rejects `record start` / `record restart` after a close row because upstream can report a recording that did not start; split those operations into separate calls. A `No recording in progress` stop failure, direct or nested, first checks the matching native receipt once. Retirement preserves the receipt and freshly checked file metadata; it never turns an existing file into a missing file merely because no recording is active. A later successful batch recording row opens its new pending path normally. Batch preflight applies the same distinct-destination rule to the steps upstream will execute: raw argument command strings exclusively when any exist, stdin arrays only otherwise; upstream-ignored stdin rows cannot fail artifact preflight, add pending recordings, or create parent directories. Parent directories are prepared for the effective steps in both modes; outside execution-directory binding, raw argument strings stay unchanged, so screenshot normalization and tracked path requests apply to stdin rows only. Outer CLI globals are removed before artifact parsing, but native batch row operands stay literal: `pdf --quick ignored.pdf` targets `--quick`, not `ignored.pdf`. Reservation checks, preparation, and requested-path presentation follow that same distinction. Recording path/URL consumers skip complete numeric `--fps` pairs without rewriting argv; native still validates rate, format and extra arguments. FPS-only calls keep the intended pinned tab.
|
|
959
965
|
|
|
960
966
|
Recording destinations are reserved within one Pi process, not across processes. Use unique paths for concurrent Pi processes: different explicit sessions can overwrite one file even when both `record stop` results are verified. Upstream’s same-session `record start` guard does not reserve the filename across other sessions.
|
|
961
967
|
- `savedFilePath` / `savedFile` for direct `download`, `pdf`, and `wait --download` / `wait -d` saved-file workflows when a host file path is reported or wrapper-verified. Batch results preserve the same fields on the relevant `batchSteps` entry. These fields are metadata only until `artifactVerification` verifies the file. Native `download <selector> <path>` always owns the click and download, including loopback links, generated Blob exports, and redirects; the wrapper does not substitute a fetch of an anchor's `href`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-agent-browser-native",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.17",
|
|
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)",
|
|
@@ -65,9 +65,9 @@
|
|
|
65
65
|
"typebox": "*"
|
|
66
66
|
},
|
|
67
67
|
"devDependencies": {
|
|
68
|
-
"@earendil-works/pi-ai": "0.
|
|
69
|
-
"@earendil-works/pi-coding-agent": "0.
|
|
70
|
-
"@earendil-works/pi-tui": "0.
|
|
68
|
+
"@earendil-works/pi-ai": "0.87.0",
|
|
69
|
+
"@earendil-works/pi-coding-agent": "0.87.0",
|
|
70
|
+
"@earendil-works/pi-tui": "0.87.0",
|
|
71
71
|
"@types/cross-spawn": "6.0.6",
|
|
72
72
|
"@types/node": "^25.9.3",
|
|
73
73
|
"@types/which": "2.0.2",
|