pi-agent-browser-native 0.2.43 → 0.2.45
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 +37 -0
- package/README.md +26 -16
- package/docs/ARCHITECTURE.md +12 -10
- package/docs/COMMAND_REFERENCE.md +49 -27
- package/docs/ELECTRON.md +1 -1
- package/docs/RELEASE.md +16 -9
- package/docs/REQUIREMENTS.md +6 -3
- package/docs/SUPPORT_MATRIX.md +18 -14
- package/docs/TOOL_CONTRACT.md +87 -46
- package/docs/platform-smoke.md +15 -9
- package/extensions/agent-browser/index.ts +29 -445
- package/extensions/agent-browser/lib/bash-guard.ts +205 -0
- package/extensions/agent-browser/lib/electron/cdp.ts +69 -0
- package/extensions/agent-browser/lib/electron/cleanup.ts +5 -58
- package/extensions/agent-browser/lib/electron/discovery.ts +2 -9
- package/extensions/agent-browser/lib/electron/launch.ts +11 -65
- package/extensions/agent-browser/lib/electron/text.ts +13 -0
- package/extensions/agent-browser/lib/fs-utils.ts +18 -0
- package/extensions/agent-browser/lib/input-modes/job.ts +207 -21
- package/extensions/agent-browser/lib/input-modes/params.ts +17 -7
- package/extensions/agent-browser/lib/input-modes/semantic-action.ts +22 -2
- package/extensions/agent-browser/lib/input-modes/types.ts +5 -1
- package/extensions/agent-browser/lib/input-modes.ts +1 -0
- package/extensions/agent-browser/lib/orchestration/browser-run/click-dispatch.ts +82 -11
- package/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.ts +153 -30
- package/extensions/agent-browser/lib/orchestration/browser-run/final-result.ts +53 -2
- package/extensions/agent-browser/lib/orchestration/browser-run/index.ts +1 -0
- package/extensions/agent-browser/lib/orchestration/browser-run/prepare.ts +751 -32
- package/extensions/agent-browser/lib/orchestration/browser-run/process-output.ts +38 -7
- package/extensions/agent-browser/lib/orchestration/browser-run/prompt-guards.ts +0 -46
- package/extensions/agent-browser/lib/orchestration/browser-run/session-state.ts +10 -1
- package/extensions/agent-browser/lib/orchestration/browser-run/types.ts +28 -1
- package/extensions/agent-browser/lib/orchestration/electron-host/index.ts +1 -6
- package/extensions/agent-browser/lib/orchestration/input-plan.ts +15 -3
- package/extensions/agent-browser/lib/orchestration/output-file.ts +86 -0
- package/extensions/agent-browser/lib/pi-tool-rendering.ts +231 -0
- package/extensions/agent-browser/lib/playbook.ts +26 -26
- package/extensions/agent-browser/lib/process.ts +1 -1
- package/extensions/agent-browser/lib/prompt-policy.ts +1 -18
- package/extensions/agent-browser/lib/results/artifact-manifest.ts +1 -4
- package/extensions/agent-browser/lib/results/artifact-state.ts +7 -3
- package/extensions/agent-browser/lib/results/contracts.ts +6 -2
- package/extensions/agent-browser/lib/results/envelope.ts +11 -2
- package/extensions/agent-browser/lib/results/network-routes.ts +7 -4
- package/extensions/agent-browser/lib/results/network.ts +7 -1
- package/extensions/agent-browser/lib/results/presentation/artifacts.ts +88 -20
- package/extensions/agent-browser/lib/results/presentation/batch.ts +84 -12
- package/extensions/agent-browser/lib/results/presentation/diagnostics.ts +81 -26
- package/extensions/agent-browser/lib/results/presentation/errors.ts +13 -0
- package/extensions/agent-browser/lib/results/presentation/registry.ts +60 -0
- package/extensions/agent-browser/lib/results/presentation.ts +10 -1
- package/extensions/agent-browser/lib/results/snapshot-high-value-controls.ts +16 -5
- package/extensions/agent-browser/lib/results/snapshot.ts +2 -0
- package/extensions/agent-browser/lib/runtime.ts +10 -1
- package/extensions/agent-browser/lib/session-page-state.ts +15 -6
- package/extensions/agent-browser/lib/web-search.ts +1 -1
- package/package.json +5 -5
- package/platform-smoke.config.mjs +15 -3
- package/scripts/doctor.mjs +70 -1
- package/scripts/platform-smoke/build-ubuntu-image.mjs +25 -0
- package/scripts/platform-smoke/crabbox-runner.mjs +62 -30
- package/scripts/platform-smoke/doctor.mjs +28 -11
- package/scripts/platform-smoke/linux-image/Dockerfile +3 -5
- package/scripts/platform-smoke/targets.mjs +60 -22
- package/scripts/platform-smoke.mjs +1 -0
- package/extensions/agent-browser/lib/orchestration/browser-run/browser-action-model.ts +0 -154
|
@@ -66,6 +66,10 @@ function mergeNextActions(...groups: Array<AgentBrowserNextAction[] | undefined>
|
|
|
66
66
|
return merged.length > 0 ? merged : undefined;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
function shouldAddAnnotatedScreenshotGuidance(commandInfo: CommandInfo, args: string[] | undefined): boolean {
|
|
70
|
+
return commandInfo.command === "screenshot" && (args?.includes("--annotate") ?? false);
|
|
71
|
+
}
|
|
72
|
+
|
|
69
73
|
export async function buildToolPresentation(options: {
|
|
70
74
|
artifactManifest?: SessionArtifactManifest;
|
|
71
75
|
args?: string[];
|
|
@@ -103,7 +107,7 @@ export async function buildToolPresentation(options: {
|
|
|
103
107
|
|
|
104
108
|
const data = enrichStreamStatusData(commandInfo, envelope?.data);
|
|
105
109
|
const presentationData = redactPresentationData(commandInfo, data);
|
|
106
|
-
const artifacts = await extractFileArtifacts({ artifactRequest, commandInfo: presentationCommandInfo, cwd, data, sessionName });
|
|
110
|
+
const artifacts = await extractFileArtifacts({ artifactManifest, artifactRequest, commandInfo: presentationCommandInfo, cwd, data, sessionName });
|
|
107
111
|
const artifactVerification = buildArtifactVerificationSummary(artifacts);
|
|
108
112
|
const artifactSummary = formatArtifactSummary(artifacts);
|
|
109
113
|
const summary = artifactSummary ?? formatPresentationSummary(commandInfo, data, compiledSemanticAction);
|
|
@@ -151,6 +155,11 @@ export async function buildToolPresentation(options: {
|
|
|
151
155
|
}
|
|
152
156
|
}
|
|
153
157
|
|
|
158
|
+
if (shouldAddAnnotatedScreenshotGuidance(commandInfo, args) && presentation.content[0]?.type === "text") {
|
|
159
|
+
const guidance = "Annotated screenshot note: dense pages can produce overlapping labels. If the labels are noisy, capture a scoped element screenshot, take a non-annotated screenshot, or use snapshot -i high-value refs as the machine-readable map.";
|
|
160
|
+
presentation.content[0] = { ...presentation.content[0], text: `${presentation.content[0].text}\n\n${guidance}` };
|
|
161
|
+
}
|
|
162
|
+
|
|
154
163
|
const imagePath = artifactRequest?.absolutePath ?? extractImagePath(commandInfo, cwd, data);
|
|
155
164
|
const presentationWithImage = imagePath ? await attachInlineImage(presentation, imagePath) : presentation;
|
|
156
165
|
const compactedPresentation = await compactLargePresentationOutput({
|
|
@@ -17,6 +17,7 @@ const SNAPSHOT_HIGH_VALUE_CONTROL_ROLES = new Set([
|
|
|
17
17
|
"button",
|
|
18
18
|
"checkbox",
|
|
19
19
|
"combobox",
|
|
20
|
+
"link",
|
|
20
21
|
"menuitem",
|
|
21
22
|
"option",
|
|
22
23
|
"radio",
|
|
@@ -30,11 +31,12 @@ const SNAPSHOT_HIGH_VALUE_CONTROL_ROLE_PRIORITY: Record<string, number> = {
|
|
|
30
31
|
textbox: 1,
|
|
31
32
|
combobox: 2,
|
|
32
33
|
button: 3,
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
34
|
+
link: 4,
|
|
35
|
+
tab: 5,
|
|
36
|
+
checkbox: 6,
|
|
37
|
+
radio: 7,
|
|
38
|
+
option: 8,
|
|
39
|
+
menuitem: 9,
|
|
38
40
|
};
|
|
39
41
|
|
|
40
42
|
const SNAPSHOT_SURFACE_CONTROL_NAME_PATTERNS = [
|
|
@@ -46,6 +48,10 @@ const SNAPSHOT_PRIMARY_ACTION_BUTTON_NAME_PATTERNS = [
|
|
|
46
48
|
/^(?:add|apply|ask|confirm|connect|continue|create|launch|new|open|refresh|retry|run|save|search|send|start|submit)\b/i,
|
|
47
49
|
];
|
|
48
50
|
|
|
51
|
+
const SNAPSHOT_HIGH_VALUE_LINK_NAME_PATTERNS = [
|
|
52
|
+
/^[a-z0-9_.-]+\/[a-z0-9_.-]+$/i,
|
|
53
|
+
];
|
|
54
|
+
|
|
49
55
|
function getHighValueControlRole(entry: SnapshotRefEntry): string {
|
|
50
56
|
return entry.isEditable === true && (entry.role === "unknown" || entry.role === "generic") ? "textbox" : entry.role;
|
|
51
57
|
}
|
|
@@ -129,9 +135,14 @@ const SNAPSHOT_HIGH_VALUE_CONTROL_CATEGORY_RULES: readonly HighValueControlCateg
|
|
|
129
135
|
},
|
|
130
136
|
] as const;
|
|
131
137
|
|
|
138
|
+
function isHighValueLinkRef(entry: SnapshotRefEntry): boolean {
|
|
139
|
+
return entry.name.length > 0 && SNAPSHOT_HIGH_VALUE_LINK_NAME_PATTERNS.some((pattern) => pattern.test(entry.name));
|
|
140
|
+
}
|
|
141
|
+
|
|
132
142
|
export function isHighValueControlEntry(entry: SnapshotRefEntry): boolean {
|
|
133
143
|
const role = getHighValueControlRole(entry);
|
|
134
144
|
if (!SNAPSHOT_HIGH_VALUE_CONTROL_ROLES.has(role)) return false;
|
|
145
|
+
if (role === "link") return isHighValueLinkRef(entry);
|
|
135
146
|
if (entry.isEditable === false && (role === "searchbox" || role === "textbox" || role === "combobox")) return false;
|
|
136
147
|
return entry.name.length > 0 || isEditableControlRef(entry);
|
|
137
148
|
}
|
|
@@ -221,6 +221,7 @@ export async function buildSnapshotPresentation(
|
|
|
221
221
|
...(roleCountsText ? [`Top roles: ${roleCountsText}`] : []),
|
|
222
222
|
"",
|
|
223
223
|
"Compact snapshot view.",
|
|
224
|
+
"Viewport note: compact snapshots are DOM/signal-prioritized, not guaranteed to start with the currently scrolled viewport; use the full raw snapshot, a screenshot, or listed high-value refs when viewport context matters.",
|
|
224
225
|
];
|
|
225
226
|
|
|
226
227
|
if (fallbackPreview) {
|
|
@@ -294,6 +295,7 @@ export async function buildSnapshotPresentation(
|
|
|
294
295
|
fullOutputPath,
|
|
295
296
|
origin,
|
|
296
297
|
previewMode: fallbackPreview ? "outline" : "structured",
|
|
298
|
+
viewportOrdering: "dom-signal-prioritized",
|
|
297
299
|
spillError: spillErrorText,
|
|
298
300
|
previewRefIds: [...previewRefIds],
|
|
299
301
|
highValueControlRefIds: visibleHighValueControlEntries.map((entry) => entry.id),
|
|
@@ -619,6 +619,15 @@ export function createFreshSessionName(baseSessionName: string, ephemeralSeed: s
|
|
|
619
619
|
return `${baseSessionName}-fresh-${suffix}`;
|
|
620
620
|
}
|
|
621
621
|
|
|
622
|
+
function getSingleKeyCommandValidationError(args: string[]): string | undefined {
|
|
623
|
+
const { commandInfo, commandTokens } = parseArgvDescriptor(args);
|
|
624
|
+
const command = commandInfo.command;
|
|
625
|
+
if (command !== "press" && command !== "key" && command !== "keydown" && command !== "keyup") return undefined;
|
|
626
|
+
if (commandTokens.length === 2) return undefined;
|
|
627
|
+
const label = command === "key" ? "key/press" : command;
|
|
628
|
+
return `agent-browser ${label} accepts exactly one key argument. Do not pass a selector or ref to ${label}; focus or click the target first, then run ${command} <key> (for example: focus @e1, then press Enter).`;
|
|
629
|
+
}
|
|
630
|
+
|
|
622
631
|
export function validateToolArgs(args: string[]): string | undefined {
|
|
623
632
|
if (args.length === 0) {
|
|
624
633
|
return "`args` must contain at least one agent-browser command token.";
|
|
@@ -634,7 +643,7 @@ export function validateToolArgs(args: string[]): string | undefined {
|
|
|
634
643
|
return "Do not pass `--session-mode` in args. Use the top-level agent_browser `sessionMode` field instead, for example { args: [\"--profile\", \"Default\", \"open\", \"https://example.com\"], sessionMode: \"fresh\" }.";
|
|
635
644
|
}
|
|
636
645
|
|
|
637
|
-
return
|
|
646
|
+
return getSingleKeyCommandValidationError(args);
|
|
638
647
|
}
|
|
639
648
|
|
|
640
649
|
function getInvalidValueFlagDetails(args: string[]): InvalidValueFlagDetails | undefined {
|
|
@@ -8,6 +8,9 @@
|
|
|
8
8
|
|
|
9
9
|
import { isCloseCommand, isReadOnlyDiagnosticSessionTargetCommand } from "./command-taxonomy.js";
|
|
10
10
|
import { isRecord } from "./parsing.js";
|
|
11
|
+
import { getEditableRefEvidence } from "./results/editable-ref-evidence.js";
|
|
12
|
+
import { enrichSnapshotRefEntries, getSnapshotRefEntries } from "./results/snapshot-refs.js";
|
|
13
|
+
import { parseSnapshotLines } from "./results/snapshot-segments.js";
|
|
11
14
|
|
|
12
15
|
export interface SessionTabTarget {
|
|
13
16
|
title?: string;
|
|
@@ -21,7 +24,7 @@ interface OrderedSessionTabTarget {
|
|
|
21
24
|
|
|
22
25
|
export interface SessionRefSnapshot {
|
|
23
26
|
refIds: string[];
|
|
24
|
-
refs?: Record<string, { name: string; role: string }>;
|
|
27
|
+
refs?: Record<string, { isContentEditable?: boolean; isEditable?: boolean; name: string; role: string }>;
|
|
25
28
|
target?: SessionTabTarget;
|
|
26
29
|
}
|
|
27
30
|
|
|
@@ -230,11 +233,15 @@ function getRestoredSessionTabTarget(details: Record<string, unknown>, command:
|
|
|
230
233
|
return storedTarget;
|
|
231
234
|
}
|
|
232
235
|
|
|
233
|
-
function extractRefSnapshotRefs(data: unknown): Record<string, { name: string; role: string }> | undefined {
|
|
236
|
+
function extractRefSnapshotRefs(data: unknown): Record<string, { isContentEditable?: boolean; isEditable?: boolean; name: string; role: string }> | undefined {
|
|
234
237
|
if (!isRecord(data) || !isRecord(data.refs)) return undefined;
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
|
|
238
|
+
const snapshotLines = typeof data.snapshot === "string" ? parseSnapshotLines(data.snapshot) : [];
|
|
239
|
+
const lineByRef = new Map(snapshotLines.flatMap((line) => line.ref ? [[line.ref, line.raw] as const] : []));
|
|
240
|
+
const entries = enrichSnapshotRefEntries(getSnapshotRefEntries(data), snapshotLines);
|
|
241
|
+
const refs = Object.fromEntries(entries.flatMap((entry) => {
|
|
242
|
+
if (!/^e\d+$/.test(entry.id) || entry.role.length === 0) return [];
|
|
243
|
+
const isContentEditable = getEditableRefEvidence({ ref: entry.refData, text: lineByRef.get(entry.id) });
|
|
244
|
+
return [[entry.id, { ...(isContentEditable === true ? { isContentEditable: true } : {}), ...(entry.isEditable !== undefined ? { isEditable: entry.isEditable } : {}), name: entry.name, role: entry.role }] as const];
|
|
238
245
|
}));
|
|
239
246
|
return Object.keys(refs).length > 0 ? refs : undefined;
|
|
240
247
|
}
|
|
@@ -310,7 +317,9 @@ function getRestoredRefSnapshot(details: Record<string, unknown>): SessionRefSna
|
|
|
310
317
|
? Object.fromEntries(refIds.flatMap((refId) => {
|
|
311
318
|
const entry = refRecord[refId];
|
|
312
319
|
if (!isRecord(entry) || typeof entry.name !== "string" || typeof entry.role !== "string") return [];
|
|
313
|
-
|
|
320
|
+
const isContentEditable = typeof entry.isContentEditable === "boolean" ? entry.isContentEditable : undefined;
|
|
321
|
+
const isEditable = typeof entry.isEditable === "boolean" ? entry.isEditable : undefined;
|
|
322
|
+
return [[refId, { ...(isContentEditable !== undefined ? { isContentEditable } : {}), ...(isEditable !== undefined ? { isEditable } : {}), name: entry.name, role: entry.role }] as const];
|
|
314
323
|
}))
|
|
315
324
|
: undefined;
|
|
316
325
|
return {
|
|
@@ -653,7 +653,7 @@ export function createAgentBrowserWebSearchTool(configState: AgentBrowserConfigS
|
|
|
653
653
|
promptSnippet: "Search the live web with Exa or Brave for current or external information.",
|
|
654
654
|
promptGuidelines: [
|
|
655
655
|
"Use agent_browser_web_search when live web search would help answer the task, find current external information, or discover candidate URLs for agent_browser.",
|
|
656
|
-
"
|
|
656
|
+
"agent_browser_web_search chooses Exa or Brave from configured keys; when both are available, Exa is preferred by default unless webSearch.preferredProvider says otherwise. Use provider only when the user/config calls for a specific provider.",
|
|
657
657
|
"Prefer agent_browser_web_search over opening a search engine results page with agent_browser when a quick result list is enough; use agent_browser for interaction, DOM, screenshots, or auth.",
|
|
658
658
|
"Do not issue parallel or repeated agent_browser_web_search calls; use one high-signal query, inspect the results, then only run a focused follow-up if needed. If the provider returns HTTP 429, stop searching and tell the user the API plan/rate limit needs time or a plan change.",
|
|
659
659
|
"After using agent_browser_web_search, cite result URLs in the final answer when web evidence informed the answer.",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-agent-browser-native",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.45",
|
|
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)",
|
|
@@ -62,9 +62,9 @@
|
|
|
62
62
|
"typebox": "*"
|
|
63
63
|
},
|
|
64
64
|
"devDependencies": {
|
|
65
|
-
"@earendil-works/pi-ai": "^0.78.
|
|
66
|
-
"@earendil-works/pi-coding-agent": "^0.78.
|
|
67
|
-
"@earendil-works/pi-tui": "^0.78.
|
|
65
|
+
"@earendil-works/pi-ai": "^0.78.1",
|
|
66
|
+
"@earendil-works/pi-coding-agent": "^0.78.1",
|
|
67
|
+
"@earendil-works/pi-tui": "^0.78.1",
|
|
68
68
|
"@types/node": "^25.6.1",
|
|
69
69
|
"tsx": "^4.21.0",
|
|
70
70
|
"typebox": "^1.1.38",
|
|
@@ -80,7 +80,7 @@
|
|
|
80
80
|
"check:platform-smoke": "node --check platform-smoke.config.mjs && node --check scripts/platform-smoke.mjs && node --check scripts/platform-smoke/doctor.mjs && node --check scripts/platform-smoke/crabbox-runner.mjs && node --check scripts/platform-smoke/targets.mjs && node --check scripts/platform-smoke/artifacts.mjs && tsx --test test/platform-smoke.test.ts",
|
|
81
81
|
"smoke:platform": "node scripts/platform-smoke.mjs",
|
|
82
82
|
"smoke:platform:doctor": "node scripts/platform-smoke.mjs doctor",
|
|
83
|
-
"smoke:platform:ubuntu-image": "
|
|
83
|
+
"smoke:platform:ubuntu-image": "node scripts/platform-smoke/build-ubuntu-image.mjs",
|
|
84
84
|
"smoke:platform:macos": "node scripts/platform-smoke.mjs run --target macos",
|
|
85
85
|
"smoke:platform:ubuntu": "node scripts/platform-smoke.mjs run --target ubuntu",
|
|
86
86
|
"smoke:platform:windows-native": "node scripts/platform-smoke.mjs run --target windows-native",
|
|
@@ -3,16 +3,28 @@
|
|
|
3
3
|
|
|
4
4
|
import { CAPABILITY_BASELINE } from "./scripts/agent-browser-capability-baseline.mjs";
|
|
5
5
|
|
|
6
|
+
export const PLATFORM_SMOKE_AGENT_BROWSER_VERSION = CAPABILITY_BASELINE.targetVersion;
|
|
7
|
+
export const PLATFORM_SMOKE_UBUNTU_IMAGE = `pi-agent-browser-native-platform:node24-agent-browser${PLATFORM_SMOKE_AGENT_BROWSER_VERSION}`;
|
|
8
|
+
|
|
6
9
|
export default {
|
|
7
10
|
packageName: "pi-agent-browser-native",
|
|
8
11
|
artifactRoot: ".artifacts/platform-smoke",
|
|
9
12
|
requiredTargets: ["macos", "ubuntu", "windows-native"],
|
|
10
13
|
requiredSuites: ["platform-build", "browser-dogfood-smoke"],
|
|
14
|
+
supportedTargets: ["macos", "ubuntu", "windows-native"],
|
|
11
15
|
requiredCrabbox: {
|
|
12
16
|
install: "Homebrew package or PLATFORM_SMOKE_CRABBOX override",
|
|
13
|
-
minVersion: "0.
|
|
17
|
+
minVersion: "0.26.0",
|
|
18
|
+
},
|
|
19
|
+
macos: {
|
|
20
|
+
host: "localhost",
|
|
21
|
+
port: 22,
|
|
22
|
+
},
|
|
23
|
+
ubuntuContainerImage: PLATFORM_SMOKE_UBUNTU_IMAGE,
|
|
24
|
+
windowsParallels: {
|
|
25
|
+
sourceVm: "pi-extension-windows-template",
|
|
26
|
+
snapshot: "crabbox-ready",
|
|
14
27
|
},
|
|
15
|
-
ubuntuContainerImage: "pi-agent-browser-native-platform:node24-agent-browser0.27.1",
|
|
16
28
|
nodeValidationMajor: 22,
|
|
17
|
-
agentBrowserVersion:
|
|
29
|
+
agentBrowserVersion: PLATFORM_SMOKE_AGENT_BROWSER_VERSION,
|
|
18
30
|
};
|
package/scripts/doctor.mjs
CHANGED
|
@@ -22,6 +22,7 @@ const PACKAGE_NAME = "pi-agent-browser-native";
|
|
|
22
22
|
const REPO_URL_FRAGMENT = "github.com/fitchmultz/pi-agent-browser-native";
|
|
23
23
|
const EXTENSION_ENTRYPOINT = "extensions/agent-browser/index.ts";
|
|
24
24
|
const EXPECTED_VERSION = CAPABILITY_BASELINE.targetVersion;
|
|
25
|
+
const RECOMMENDED_PI_VERSION = "0.78.1";
|
|
25
26
|
const DEFAULT_AGENT_DIR = resolve(homedir(), ".pi/agent");
|
|
26
27
|
const THIS_PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
27
28
|
|
|
@@ -29,6 +30,27 @@ export function normalizeAgentBrowserVersion(output) {
|
|
|
29
30
|
return String(output ?? "").trim().replace(/^agent-browser\s+/, "");
|
|
30
31
|
}
|
|
31
32
|
|
|
33
|
+
export function normalizePiVersion(output) {
|
|
34
|
+
return String(output ?? "").trim().replace(/^pi\s+/, "");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function parseVersionParts(version) {
|
|
38
|
+
const match = String(version ?? "").match(/^(\d+)\.(\d+)\.(\d+)(?:\b|[-+])/);
|
|
39
|
+
if (!match) return undefined;
|
|
40
|
+
return match.slice(1).map((part) => Number.parseInt(part, 10));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function versionAtLeast(actual, minimum) {
|
|
44
|
+
const actualParts = parseVersionParts(actual);
|
|
45
|
+
const minimumParts = parseVersionParts(minimum);
|
|
46
|
+
if (!actualParts || !minimumParts) return undefined;
|
|
47
|
+
for (let index = 0; index < minimumParts.length; index += 1) {
|
|
48
|
+
if (actualParts[index] > minimumParts[index]) return true;
|
|
49
|
+
if (actualParts[index] < minimumParts[index]) return false;
|
|
50
|
+
}
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
|
|
32
54
|
function printHelp() {
|
|
33
55
|
console.log(`pi-agent-browser-doctor
|
|
34
56
|
|
|
@@ -45,7 +67,8 @@ Options:
|
|
|
45
67
|
Checks:
|
|
46
68
|
1. agent-browser is installed on PATH.
|
|
47
69
|
2. agent-browser --version matches the package capability baseline.
|
|
48
|
-
3.
|
|
70
|
+
3. pi --version is at least the recommended Pi floor for this release.
|
|
71
|
+
4. Pi settings and repo-local autoload locations do not point at multiple active pi-agent-browser-native sources.
|
|
49
72
|
|
|
50
73
|
Examples:
|
|
51
74
|
pi-agent-browser-doctor
|
|
@@ -101,6 +124,11 @@ async function defaultRunAgentBrowser(args) {
|
|
|
101
124
|
return `${stdout}${stderr}`;
|
|
102
125
|
}
|
|
103
126
|
|
|
127
|
+
async function defaultRunPi(args) {
|
|
128
|
+
const { stdout, stderr } = await execFile("pi", args, { maxBuffer: 1024 * 1024 });
|
|
129
|
+
return `${stdout}${stderr}`;
|
|
130
|
+
}
|
|
131
|
+
|
|
104
132
|
async function defaultPathExists(path) {
|
|
105
133
|
try {
|
|
106
134
|
await access(path);
|
|
@@ -270,6 +298,43 @@ async function collectRepoLocalSources({ cwd, pathExists }) {
|
|
|
270
298
|
return sources;
|
|
271
299
|
}
|
|
272
300
|
|
|
301
|
+
async function checkPiVersion({ runPi }) {
|
|
302
|
+
try {
|
|
303
|
+
const rawOutput = await runPi(["--version"]);
|
|
304
|
+
const version = normalizePiVersion(rawOutput);
|
|
305
|
+
const supported = versionAtLeast(version, RECOMMENDED_PI_VERSION);
|
|
306
|
+
if (supported === false) {
|
|
307
|
+
return {
|
|
308
|
+
status: "warn",
|
|
309
|
+
title: `Pi ${RECOMMENDED_PI_VERSION} or newer is recommended; found ${version || "<empty>"}.`,
|
|
310
|
+
lines: [
|
|
311
|
+
"This package does not hard-pin Pi 0.78.1, but this release was audited against Pi 0.78.1 extension/package behavior.",
|
|
312
|
+
"Update Pi before release validation or lifecycle debugging if you see tool routing, /reload, exact-session, or package-install differences.",
|
|
313
|
+
],
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
if (supported === undefined) {
|
|
317
|
+
return {
|
|
318
|
+
status: "warn",
|
|
319
|
+
title: `Could not parse pi --version output: ${version || "<empty>"}.`,
|
|
320
|
+
lines: [`Pi ${RECOMMENDED_PI_VERSION} or newer is recommended for this release's validation baseline.`],
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
return { status: "pass", title: `Pi version is within the recommended baseline: ${version}`, lines: [] };
|
|
324
|
+
} catch (error) {
|
|
325
|
+
const code = error && typeof error === "object" ? error.code : undefined;
|
|
326
|
+
return {
|
|
327
|
+
status: "warn",
|
|
328
|
+
title: "Could not inspect pi --version.",
|
|
329
|
+
lines: [
|
|
330
|
+
`Pi ${RECOMMENDED_PI_VERSION} or newer is recommended for this release's validation baseline, but it is not hard-pinned as a runtime requirement.`,
|
|
331
|
+
"Make sure the same shell that launches pi can run `pi --version` when debugging lifecycle or package-install behavior.",
|
|
332
|
+
code && code !== "ENOENT" ? `Spawn error: ${String(code)}` : undefined,
|
|
333
|
+
].filter(Boolean),
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
273
338
|
async function checkAgentBrowserVersion({ runAgentBrowser }) {
|
|
274
339
|
try {
|
|
275
340
|
const rawOutput = await runAgentBrowser(["--version"]);
|
|
@@ -358,6 +423,7 @@ export async function evaluateDoctor(options = {}) {
|
|
|
358
423
|
const readText = options.readText ?? ((path) => readFile(path, "utf8"));
|
|
359
424
|
const pathExists = options.pathExists ?? defaultPathExists;
|
|
360
425
|
const runAgentBrowser = options.runAgentBrowser ?? defaultRunAgentBrowser;
|
|
426
|
+
const runPi = options.runPi ?? defaultRunPi;
|
|
361
427
|
const checks = [];
|
|
362
428
|
const failures = [];
|
|
363
429
|
const warnings = [];
|
|
@@ -366,6 +432,9 @@ export async function evaluateDoctor(options = {}) {
|
|
|
366
432
|
checks.push(versionCheck);
|
|
367
433
|
if (versionCheck.status === "fail") failures.push(versionCheck);
|
|
368
434
|
|
|
435
|
+
const piVersionCheck = await checkPiVersion({ runPi });
|
|
436
|
+
checks.push(piVersionCheck);
|
|
437
|
+
|
|
369
438
|
if (!options.skipSourceCheck) {
|
|
370
439
|
const sourceCheck = await checkPiSources({ cwd, agentDir, settingsPaths, readText, pathExists });
|
|
371
440
|
checks.push(sourceCheck);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
|
|
4
|
+
import { CAPABILITY_BASELINE } from "../agent-browser-capability-baseline.mjs";
|
|
5
|
+
|
|
6
|
+
const version = CAPABILITY_BASELINE.targetVersion;
|
|
7
|
+
const image = `pi-agent-browser-native-platform:node24-agent-browser${version}`;
|
|
8
|
+
const args = [
|
|
9
|
+
"build",
|
|
10
|
+
"-t",
|
|
11
|
+
image,
|
|
12
|
+
"--build-arg",
|
|
13
|
+
`AGENT_BROWSER_VERSION=${version}`,
|
|
14
|
+
"-f",
|
|
15
|
+
"scripts/platform-smoke/linux-image/Dockerfile",
|
|
16
|
+
".",
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
console.log(`Building ${image}`);
|
|
20
|
+
const result = spawnSync("docker", args, { stdio: "inherit" });
|
|
21
|
+
if (result.error) {
|
|
22
|
+
console.error(result.error.message);
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
process.exit(result.status ?? 1);
|
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
import { spawn } from "node:child_process";
|
|
4
4
|
|
|
5
|
+
import { CAPABILITY_BASELINE } from "../agent-browser-capability-baseline.mjs";
|
|
6
|
+
|
|
7
|
+
const DEFAULT_UBUNTU_IMAGE = `pi-agent-browser-native-platform:node24-agent-browser${CAPABILITY_BASELINE.targetVersion}`;
|
|
8
|
+
|
|
5
9
|
function env(name) {
|
|
6
10
|
return process.env[name] ?? "";
|
|
7
11
|
}
|
|
@@ -14,49 +18,77 @@ function packageSlug(config = {}) {
|
|
|
14
18
|
return process.env.PLATFORM_SMOKE_PACKAGE_SLUG || config.packageName || "pi-agent-browser-native";
|
|
15
19
|
}
|
|
16
20
|
|
|
17
|
-
export function
|
|
21
|
+
export function describeTarget(targetName, config = {}) {
|
|
22
|
+
const slug = packageSlug(config);
|
|
18
23
|
switch (targetName) {
|
|
19
24
|
case "macos": {
|
|
20
25
|
const user = env("PLATFORM_SMOKE_MAC_USER") || env("USER");
|
|
21
|
-
const host = env("PLATFORM_SMOKE_MAC_HOST") || "localhost";
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
26
|
+
const host = env("PLATFORM_SMOKE_MAC_HOST") || config.macos?.host || "localhost";
|
|
27
|
+
const port = String(env("PLATFORM_SMOKE_MAC_PORT") || config.macos?.port || 22);
|
|
28
|
+
const workRoot = env("PLATFORM_SMOKE_MAC_WORK_ROOT") || config.macos?.workRoot || `/Users/${user}/crabbox/${slug}`;
|
|
29
|
+
return {
|
|
30
|
+
provider: "ssh",
|
|
31
|
+
crabboxTarget: "macos",
|
|
32
|
+
shell: "posix",
|
|
33
|
+
workRoot,
|
|
34
|
+
args: [
|
|
35
|
+
"--provider", "ssh",
|
|
36
|
+
"--target", "macos",
|
|
37
|
+
"--static-host", host,
|
|
38
|
+
"--static-user", user,
|
|
39
|
+
"--static-port", port,
|
|
40
|
+
"--static-work-root", workRoot,
|
|
41
|
+
],
|
|
42
|
+
};
|
|
31
43
|
}
|
|
32
44
|
case "ubuntu": {
|
|
33
|
-
const image = env("PLATFORM_SMOKE_UBUNTU_IMAGE") || config.ubuntuContainerImage ||
|
|
34
|
-
return
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
"
|
|
38
|
-
|
|
45
|
+
const image = env("PLATFORM_SMOKE_UBUNTU_IMAGE") || config.ubuntuContainerImage || DEFAULT_UBUNTU_IMAGE;
|
|
46
|
+
return {
|
|
47
|
+
provider: "local-container",
|
|
48
|
+
crabboxTarget: "linux",
|
|
49
|
+
shell: "posix",
|
|
50
|
+
image,
|
|
51
|
+
workRoot: config.localContainer?.workRoot || "/work/crabbox",
|
|
52
|
+
args: [
|
|
53
|
+
"--provider", "local-container",
|
|
54
|
+
"--target", "linux",
|
|
55
|
+
"--local-container-image", image,
|
|
56
|
+
],
|
|
57
|
+
};
|
|
39
58
|
}
|
|
40
59
|
case "windows-native": {
|
|
41
|
-
const vm = env("PLATFORM_SMOKE_WINDOWS_VM") || "pi-extension-windows-template";
|
|
42
|
-
const snapshot = env("PLATFORM_SMOKE_WINDOWS_SNAPSHOT") || "crabbox-ready";
|
|
43
|
-
const user = env("PLATFORM_SMOKE_WINDOWS_USER") || env("USER");
|
|
44
|
-
const workRoot = env("PLATFORM_SMOKE_WINDOWS_WORK_ROOT") || `C:\\crabbox\\${
|
|
45
|
-
return
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
"
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
60
|
+
const vm = env("PLATFORM_SMOKE_WINDOWS_VM") || config.windowsParallels?.sourceVm || "pi-extension-windows-template";
|
|
61
|
+
const snapshot = env("PLATFORM_SMOKE_WINDOWS_SNAPSHOT") || config.windowsParallels?.snapshot || "crabbox-ready";
|
|
62
|
+
const user = env("PLATFORM_SMOKE_WINDOWS_USER") || config.windowsParallels?.user || env("USER");
|
|
63
|
+
const workRoot = env("PLATFORM_SMOKE_WINDOWS_WORK_ROOT") || config.windowsParallels?.workRoot || `C:\\crabbox\\${slug}`;
|
|
64
|
+
return {
|
|
65
|
+
provider: "parallels",
|
|
66
|
+
crabboxTarget: "windows",
|
|
67
|
+
shell: "powershell",
|
|
68
|
+
workRoot,
|
|
69
|
+
windowsMode: "normal",
|
|
70
|
+
sourceVm: vm,
|
|
71
|
+
snapshot,
|
|
72
|
+
args: [
|
|
73
|
+
"--provider", "parallels",
|
|
74
|
+
"--target", "windows",
|
|
75
|
+
"--windows-mode", "normal",
|
|
76
|
+
"--parallels-source", vm,
|
|
77
|
+
"--parallels-source-snapshot", snapshot,
|
|
78
|
+
"--parallels-user", user,
|
|
79
|
+
"--parallels-work-root", workRoot,
|
|
80
|
+
],
|
|
81
|
+
};
|
|
54
82
|
}
|
|
55
83
|
default:
|
|
56
84
|
throw new Error(`unknown platform smoke target: ${targetName}`);
|
|
57
85
|
}
|
|
58
86
|
}
|
|
59
87
|
|
|
88
|
+
export function buildTargetBaseArgs(targetName, config = {}) {
|
|
89
|
+
return describeTarget(targetName, config).args;
|
|
90
|
+
}
|
|
91
|
+
|
|
60
92
|
export function leaseIdFor(targetName, slug) {
|
|
61
93
|
if (targetName === "macos") return "static_localhost";
|
|
62
94
|
return slug;
|
|
@@ -4,6 +4,10 @@ import { execFileSync, execSync } from "node:child_process";
|
|
|
4
4
|
import { accessSync, constants, mkdirSync, unlinkSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { resolve } from "node:path";
|
|
6
6
|
|
|
7
|
+
import { CAPABILITY_BASELINE } from "../agent-browser-capability-baseline.mjs";
|
|
8
|
+
|
|
9
|
+
const DEFAULT_UBUNTU_IMAGE = `pi-agent-browser-native-platform:node24-agent-browser${CAPABILITY_BASELINE.targetVersion}`;
|
|
10
|
+
|
|
7
11
|
function env(name) {
|
|
8
12
|
return process.env[name] ?? "";
|
|
9
13
|
}
|
|
@@ -105,6 +109,17 @@ function checkForbiddenProjectFiles(failures) {
|
|
|
105
109
|
}
|
|
106
110
|
|
|
107
111
|
function crabboxProviders(cbox) {
|
|
112
|
+
const jsonOutput = silent(cbox, ["providers", "--json"]);
|
|
113
|
+
if (jsonOutput) {
|
|
114
|
+
try {
|
|
115
|
+
const parsed = JSON.parse(jsonOutput);
|
|
116
|
+
if (Array.isArray(parsed)) return parsed.map((provider) => provider.name ?? provider.id ?? provider.provider).filter(Boolean);
|
|
117
|
+
if (Array.isArray(parsed.providers)) return parsed.providers.map((provider) => provider.name ?? provider.id ?? provider.provider).filter(Boolean);
|
|
118
|
+
if (typeof parsed === "object" && parsed) return Object.keys(parsed.providers ?? parsed);
|
|
119
|
+
} catch {
|
|
120
|
+
// Fall through to text parsing for older or non-JSON provider output.
|
|
121
|
+
}
|
|
122
|
+
}
|
|
108
123
|
const output = silent(cbox, ["providers"]);
|
|
109
124
|
if (!output) return [];
|
|
110
125
|
return output.split(/\r?\n/)
|
|
@@ -209,25 +224,27 @@ export async function runDoctor(config) {
|
|
|
209
224
|
console.log("\n── Crabbox providers ──");
|
|
210
225
|
if (cboxPath) {
|
|
211
226
|
checkRequiredProviders(cbox, failures);
|
|
212
|
-
const ubuntuImage = env("PLATFORM_SMOKE_UBUNTU_IMAGE") || config?.ubuntuContainerImage ||
|
|
227
|
+
const ubuntuImage = env("PLATFORM_SMOKE_UBUNTU_IMAGE") || config?.ubuntuContainerImage || DEFAULT_UBUNTU_IMAGE;
|
|
213
228
|
checkCrabboxProvider(cbox, ["--provider", "local-container", "--local-container-image", ubuntuImage], "ubuntu local-container", failures);
|
|
214
229
|
const macUser = env("PLATFORM_SMOKE_MAC_USER") || env("USER");
|
|
215
|
-
const macHost = env("PLATFORM_SMOKE_MAC_HOST") || "localhost";
|
|
216
|
-
const
|
|
217
|
-
|
|
230
|
+
const macHost = env("PLATFORM_SMOKE_MAC_HOST") || config?.macos?.host || "localhost";
|
|
231
|
+
const macPort = String(env("PLATFORM_SMOKE_MAC_PORT") || config?.macos?.port || 22);
|
|
232
|
+
const macRoot = env("PLATFORM_SMOKE_MAC_WORK_ROOT") || config?.macos?.workRoot || `/Users/${macUser}/crabbox/${packageName}`;
|
|
233
|
+
checkCrabboxProvider(cbox, ["--provider", "ssh", "--target", "macos", "--static-host", macHost, "--static-user", macUser, "--static-port", macPort, "--static-work-root", macRoot], "macOS ssh", failures);
|
|
218
234
|
}
|
|
219
235
|
|
|
220
236
|
console.log("\n── Docker / Ubuntu ──");
|
|
221
237
|
const dockerVersion = shell("docker info --format '{{.ServerVersion}}'");
|
|
222
238
|
if (dockerVersion) ok(`Docker ${dockerVersion}`);
|
|
223
239
|
else fail("Docker is not available or not running", failures);
|
|
224
|
-
const ubuntuImage = env("PLATFORM_SMOKE_UBUNTU_IMAGE") || config?.ubuntuContainerImage ||
|
|
240
|
+
const ubuntuImage = env("PLATFORM_SMOKE_UBUNTU_IMAGE") || config?.ubuntuContainerImage || DEFAULT_UBUNTU_IMAGE;
|
|
225
241
|
ok(`Ubuntu image: ${ubuntuImage}`);
|
|
226
242
|
|
|
227
243
|
console.log("\n── macOS SSH ──");
|
|
228
244
|
const sshUser = env("PLATFORM_SMOKE_MAC_USER") || env("USER");
|
|
229
|
-
const sshHost = env("PLATFORM_SMOKE_MAC_HOST") || "localhost";
|
|
230
|
-
const
|
|
245
|
+
const sshHost = env("PLATFORM_SMOKE_MAC_HOST") || config?.macos?.host || "localhost";
|
|
246
|
+
const sshPort = String(env("PLATFORM_SMOKE_MAC_PORT") || config?.macos?.port || 22);
|
|
247
|
+
const sshProbe = shell(`ssh -o BatchMode=yes -o ConnectTimeout=5 -o StrictHostKeyChecking=no -p ${sshPort} ${sshUser}@${sshHost} 'node --version && npm --version && git --version && agent-browser --version'`);
|
|
231
248
|
if (sshProbe) {
|
|
232
249
|
ok(`SSH ${sshUser}@${sshHost}: ${sshProbe.split(/\r?\n/).join(" | ")}`);
|
|
233
250
|
if (agentBrowserVersion && !sshProbe.includes(agentBrowserVersion)) fail(`macOS SSH agent-browser does not match expected ${agentBrowserVersion}`, failures);
|
|
@@ -241,10 +258,10 @@ export async function runDoctor(config) {
|
|
|
241
258
|
fail("prlctl not found", failures);
|
|
242
259
|
} else {
|
|
243
260
|
ok("prlctl found");
|
|
244
|
-
const vmName = env("PLATFORM_SMOKE_WINDOWS_VM") || "pi-extension-windows-template";
|
|
245
|
-
const snapshot = env("PLATFORM_SMOKE_WINDOWS_SNAPSHOT") || "crabbox-ready";
|
|
246
|
-
const user = env("PLATFORM_SMOKE_WINDOWS_USER") || env("USER");
|
|
247
|
-
const workRoot = env("PLATFORM_SMOKE_WINDOWS_WORK_ROOT") || `C:\\crabbox\\${packageName}`;
|
|
261
|
+
const vmName = env("PLATFORM_SMOKE_WINDOWS_VM") || config?.windowsParallels?.sourceVm || "pi-extension-windows-template";
|
|
262
|
+
const snapshot = env("PLATFORM_SMOKE_WINDOWS_SNAPSHOT") || config?.windowsParallels?.snapshot || "crabbox-ready";
|
|
263
|
+
const user = env("PLATFORM_SMOKE_WINDOWS_USER") || config?.windowsParallels?.user || env("USER");
|
|
264
|
+
const workRoot = env("PLATFORM_SMOKE_WINDOWS_WORK_ROOT") || config?.windowsParallels?.workRoot || `C:\\crabbox\\${packageName}`;
|
|
248
265
|
const list = shell("prlctl list -a --no-header 2>/dev/null");
|
|
249
266
|
if (!list) {
|
|
250
267
|
fail("prlctl list returned no VMs", failures);
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
# Local Crabbox Ubuntu/Linux target image for pi-agent-browser-native platform smoke.
|
|
2
|
-
# Build with:
|
|
3
|
-
#
|
|
4
|
-
# --build-arg AGENT_BROWSER_VERSION=0.27.1 \
|
|
5
|
-
# -f scripts/platform-smoke/linux-image/Dockerfile .
|
|
2
|
+
# Build with npm run smoke:platform:ubuntu-image so the agent-browser version
|
|
3
|
+
# comes from scripts/agent-browser-capability-baseline.mjs.
|
|
6
4
|
|
|
7
5
|
FROM node:24-bookworm
|
|
8
6
|
|
|
9
|
-
ARG AGENT_BROWSER_VERSION
|
|
7
|
+
ARG AGENT_BROWSER_VERSION
|
|
10
8
|
|
|
11
9
|
USER root
|
|
12
10
|
RUN apt-get update \
|