pi-agent-browser-native 0.2.39 → 0.2.41

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-agent-browser-native",
3
- "version": "0.2.39",
3
+ "version": "0.2.41",
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)",
@@ -27,11 +27,13 @@
27
27
  "node": ">=22.19.0"
28
28
  },
29
29
  "bin": {
30
+ "pi-agent-browser-config": "scripts/config.mjs",
30
31
  "pi-agent-browser-doctor": "scripts/doctor.mjs"
31
32
  },
32
33
  "files": [
33
34
  "extensions",
34
35
  "platform-smoke.config.mjs",
36
+ "scripts/config.mjs",
35
37
  "scripts/doctor.mjs",
36
38
  "scripts/agent-browser-capability-baseline.mjs",
37
39
  "scripts/platform-smoke.mjs",
@@ -0,0 +1,381 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Purpose: Manage pi-agent-browser-native package config under Pi-scoped config paths.
4
+ * Responsibilities: Thin CLI argument parsing and config-file mutation around the shared config policy; preserve safe permissions and avoid echoing secrets.
5
+ * Scope: Maintainer/user setup CLI only; canonical config validation, merge, provider descriptors, and status projection live in extensions/agent-browser/lib/config-policy.js.
6
+ */
7
+
8
+ import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
9
+ import { dirname } from "node:path";
10
+ import process from "node:process";
11
+
12
+ import {
13
+ AGENT_BROWSER_CONFIG_ENV,
14
+ BRAVE_API_KEY_ENV,
15
+ DEFAULT_WEB_SEARCH_PROVIDER,
16
+ EXA_API_KEY_ENV,
17
+ WEB_SEARCH_PROVIDERS,
18
+ formatBrowserExecutableStatus,
19
+ formatBrowserProfileStatus,
20
+ getAgentBrowserConfigPaths,
21
+ getCredentialSourceSummary,
22
+ getWebSearchProviderConfigKey,
23
+ getWebSearchProviderEnvVar,
24
+ getWebSearchProviderLabel,
25
+ isProjectSafeCredentialValueForProvider,
26
+ isWebSearchProvider,
27
+ loadAgentBrowserConfigStateSync,
28
+ summarizeConfigFiles,
29
+ } from "../extensions/agent-browser/lib/config-policy.js";
30
+
31
+ const DEFAULT_CONFIG = { version: 1 };
32
+
33
+ class UsageError extends Error {
34
+ constructor(message) {
35
+ super(message);
36
+ this.name = "UsageError";
37
+ }
38
+ }
39
+
40
+ function usage() {
41
+ return `pi-agent-browser-config
42
+
43
+ Usage:
44
+ pi-agent-browser-config paths
45
+ pi-agent-browser-config show
46
+ pi-agent-browser-config web-search status
47
+ pi-agent-browser-config web-search set-key --stdin --provider <exa|brave> [--global]
48
+ pi-agent-browser-config web-search set-env <ENV_VAR> [--provider brave|exa] [--global|--project]
49
+ pi-agent-browser-config web-search set-command <command> --provider <exa|brave> [--global]
50
+ pi-agent-browser-config web-search clear --provider <exa|brave|all> [--global|--project]
51
+ pi-agent-browser-config web-search prefer <exa|brave|auto> [--global|--project]
52
+ pi-agent-browser-config web-search enable [--global|--project]
53
+ pi-agent-browser-config web-search disable [--global|--project]
54
+ pi-agent-browser-config browser profile status
55
+ pi-agent-browser-config browser profile set <name|path> [--policy explicit-only|authenticated-only|always] [--global|--project]
56
+ pi-agent-browser-config browser profile clear [--global|--project]
57
+ pi-agent-browser-config browser executable status
58
+ pi-agent-browser-config browser executable set <path> [--global]
59
+ pi-agent-browser-config browser executable clear [--global|--project]
60
+
61
+ Notes:
62
+ Global config: ~/.pi/config/pi-agent-browser-native/config.json
63
+ Project config: .pi/config/pi-agent-browser-native/config.json
64
+ Override: ${AGENT_BROWSER_CONFIG_ENV}=/path/to/config.json
65
+ Project-local plaintext, custom env aliases, interpolation-literal, malformed, and command-backed web-search keys are refused; use matching ${EXA_API_KEY_ENV} or ${BRAVE_API_KEY_ENV} set-env references there.
66
+ Use --provider for set-key, set-command, and clear; set-env infers exa/brave from ${EXA_API_KEY_ENV} or ${BRAVE_API_KEY_ENV}.
67
+ `;
68
+ }
69
+
70
+ function parseArgs(argv) {
71
+ const positional = [];
72
+ const flags = new Map();
73
+ for (let index = 0; index < argv.length; index += 1) {
74
+ const arg = argv[index];
75
+ if (!arg.startsWith("--")) {
76
+ positional.push(arg);
77
+ continue;
78
+ }
79
+ if (arg === "--global" || arg === "--project" || arg === "--stdin" || arg === "--help") {
80
+ flags.set(arg, true);
81
+ continue;
82
+ }
83
+ if (arg === "--policy" || arg === "--provider") {
84
+ const value = argv[index + 1];
85
+ if (!value || value.startsWith("--")) throw new UsageError(`${arg} requires a value.`);
86
+ flags.set(arg, value);
87
+ index += 1;
88
+ continue;
89
+ }
90
+ throw new UsageError(`Unknown option: ${arg}`);
91
+ }
92
+ if (flags.get("--global") && flags.get("--project")) throw new UsageError("Use only one of --global or --project.");
93
+ return { flags, positional };
94
+ }
95
+
96
+ function readConfig(path) {
97
+ if (!existsSync(path)) return { ...DEFAULT_CONFIG };
98
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
99
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error(`${path} must contain a JSON object.`);
100
+ return { ...DEFAULT_CONFIG, ...parsed };
101
+ }
102
+
103
+ function writeConfig(path, config) {
104
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
105
+ writeFileSync(path, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 });
106
+ try {
107
+ chmodSync(dirname(path), 0o700);
108
+ chmodSync(path, 0o600);
109
+ } catch {
110
+ // Best effort on platforms/filesystems that do not support POSIX modes.
111
+ }
112
+ }
113
+
114
+ function selectWritePath(flags) {
115
+ const paths = getAgentBrowserConfigPaths();
116
+ if (flags.get("--project")) return { path: paths.project, scope: "project" };
117
+ return { path: paths.global, scope: "global" };
118
+ }
119
+
120
+ function validateWebSearchProviderArg(provider, { allowAll = false } = {}) {
121
+ if (isWebSearchProvider(provider) || (allowAll && provider === "all")) return provider;
122
+ throw new UsageError(`--provider must be one of ${allowAll ? `${WEB_SEARCH_PROVIDERS.join(", ")}, all` : WEB_SEARCH_PROVIDERS.join(", ")}.`);
123
+ }
124
+
125
+ function inferWebSearchProviderFromEnvName(envName) {
126
+ for (const provider of WEB_SEARCH_PROVIDERS) {
127
+ if (envName === getWebSearchProviderEnvVar(provider)) return provider;
128
+ }
129
+ return undefined;
130
+ }
131
+
132
+ function getWebSearchProvider(flags, options = {}) {
133
+ const configured = flags.get("--provider");
134
+ if (configured) return validateWebSearchProviderArg(configured, options);
135
+ const inferred = options.envName ? inferWebSearchProviderFromEnvName(options.envName) : undefined;
136
+ if (inferred) return inferred;
137
+ throw new UsageError(options.allowAll ? "--provider is required and must be exa, brave, or all." : "--provider is required and must be exa or brave.");
138
+ }
139
+
140
+ function setWebSearchCredential(config, provider, value) {
141
+ config.webSearch = { ...(config.webSearch ?? {}), [getWebSearchProviderConfigKey(provider)]: value };
142
+ }
143
+
144
+ function clearWebSearchCredential(config, provider) {
145
+ if (config.webSearch) delete config.webSearch[getWebSearchProviderConfigKey(provider)];
146
+ }
147
+
148
+ function printPaths() {
149
+ const paths = getAgentBrowserConfigPaths();
150
+ console.log(`Global: ${paths.global}`);
151
+ console.log(`Project: ${paths.project}`);
152
+ console.log(`Override: ${paths.override ?? `${AGENT_BROWSER_CONFIG_ENV} not set`}`);
153
+ }
154
+
155
+ function printStatus() {
156
+ const state = loadAgentBrowserConfigStateSync({ cwd: process.cwd(), env: process.env });
157
+ printPaths();
158
+ console.log("");
159
+ console.log("Config files:");
160
+ for (const file of summarizeConfigFiles(state)) {
161
+ console.log(` ${file.scope}: ${file.path} ${file.exists ? "[exists]" : "[missing]"}`);
162
+ }
163
+ console.log("");
164
+ console.log("Effective config:");
165
+ console.log(` webSearch.enabled: ${state.webSearchEnabled ? "true" : "false"}`);
166
+ console.log(` webSearch.preferredProvider: ${state.config.webSearch?.preferredProvider ?? `auto (default ${DEFAULT_WEB_SEARCH_PROVIDER})`}`);
167
+ for (const provider of WEB_SEARCH_PROVIDERS) {
168
+ const field = getWebSearchProviderConfigKey(provider);
169
+ console.log(` webSearch.${field}: ${getCredentialSourceSummary(state.webSearchCredentialSources[provider], provider)}`);
170
+ }
171
+ console.log(` browser.defaultProfile: ${formatBrowserProfileStatus(state)}`);
172
+ console.log(` browser.executablePath: ${formatBrowserExecutableStatus(state)}`);
173
+ if (state.layers.length === 0) console.log(" layers: none");
174
+ if (state.warnings.length > 0) {
175
+ console.log("");
176
+ console.log("Warnings:");
177
+ for (const warning of state.warnings) console.log(` - ${warning}`);
178
+ }
179
+ if (state.errors.length > 0) {
180
+ console.log("");
181
+ console.log("Validation errors:");
182
+ for (const error of state.errors) console.log(` - ${error}`);
183
+ }
184
+ }
185
+
186
+ async function readSecretFromStdin(useStdin) {
187
+ if (!useStdin) throw new UsageError("set-key requires --stdin so the key is not passed through argv or an echoed prompt.");
188
+ let input = "";
189
+ for await (const chunk of process.stdin) input += chunk;
190
+ const value = input.trim();
191
+ if (!value) throw new UsageError("No key was provided on stdin.");
192
+ return value;
193
+ }
194
+
195
+ function mutateConfig(path, mutate) {
196
+ const config = readConfig(path);
197
+ mutate(config);
198
+ writeConfig(path, config);
199
+ }
200
+
201
+ async function handleWebSearch(args, flags) {
202
+ const action = args[0];
203
+ if (action === "status") {
204
+ printStatus();
205
+ return;
206
+ }
207
+ if (action === "set-key") {
208
+ const provider = getWebSearchProvider(flags);
209
+ if (flags.get("--project")) throw new UsageError(`Plaintext ${getWebSearchProviderLabel(provider)} keys cannot be written to project-local config. Use set-env or set-command.`);
210
+ const key = await readSecretFromStdin(Boolean(flags.get("--stdin")));
211
+ const { path } = selectWritePath(flags);
212
+ mutateConfig(path, (config) => {
213
+ setWebSearchCredential(config, provider, key);
214
+ });
215
+ console.log(`Saved ${getWebSearchProviderLabel(provider)} key to global config: ${path}`);
216
+ return;
217
+ }
218
+ if (action === "set-env") {
219
+ const envName = args[1];
220
+ if (!envName || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(envName)) throw new UsageError("set-env requires a valid environment variable name.");
221
+ const provider = getWebSearchProvider(flags, { envName });
222
+ const envReference = `$${envName}`;
223
+ if (flags.get("--project") && !isProjectSafeCredentialValueForProvider(envReference, provider)) {
224
+ throw new UsageError(`Project-local ${getWebSearchProviderLabel(provider)} env references must use ${getWebSearchProviderEnvVar(provider)} exactly; custom env aliases belong in global config or ${AGENT_BROWSER_CONFIG_ENV}.`);
225
+ }
226
+ const { path, scope } = selectWritePath(flags);
227
+ mutateConfig(path, (config) => {
228
+ setWebSearchCredential(config, provider, envReference);
229
+ });
230
+ console.log(`Saved ${getWebSearchProviderLabel(provider)} ${scope} env reference to: ${path}`);
231
+ return;
232
+ }
233
+ if (action === "set-command") {
234
+ const provider = getWebSearchProvider(flags);
235
+ if (flags.get("--project")) throw new UsageError(`Command-backed ${getWebSearchProviderLabel(provider)} keys cannot be written to project-local config. Use set-env there.`);
236
+ const command = args.slice(1).join(" ").trim();
237
+ if (!command) throw new UsageError("set-command requires a command string.");
238
+ const { path, scope } = selectWritePath(flags);
239
+ mutateConfig(path, (config) => {
240
+ setWebSearchCredential(config, provider, `!${command}`);
241
+ });
242
+ console.log(`Saved ${getWebSearchProviderLabel(provider)} ${scope} command source to: ${path}`);
243
+ return;
244
+ }
245
+ if (action === "clear") {
246
+ const provider = getWebSearchProvider(flags, { allowAll: true });
247
+ const { path, scope } = selectWritePath(flags);
248
+ mutateConfig(path, (config) => {
249
+ if (provider === "all") {
250
+ for (const entry of WEB_SEARCH_PROVIDERS) clearWebSearchCredential(config, entry);
251
+ } else {
252
+ clearWebSearchCredential(config, provider);
253
+ }
254
+ });
255
+ console.log(`Cleared ${provider === "all" ? "all web-search" : getWebSearchProviderLabel(provider)} credential source in ${scope} config: ${path}`);
256
+ return;
257
+ }
258
+ if (action === "prefer") {
259
+ const provider = args[1];
260
+ if (!provider || (!isWebSearchProvider(provider) && provider !== "auto")) throw new UsageError("prefer requires exa, brave, or auto.");
261
+ const { path, scope } = selectWritePath(flags);
262
+ mutateConfig(path, (config) => {
263
+ config.webSearch = { ...(config.webSearch ?? {}) };
264
+ if (provider === "auto") delete config.webSearch.preferredProvider;
265
+ else config.webSearch.preferredProvider = provider;
266
+ });
267
+ console.log(`${provider === "auto" ? "Cleared" : "Saved"} web-search preferred provider in ${scope} config: ${path}`);
268
+ return;
269
+ }
270
+ if (action === "enable" || action === "disable") {
271
+ const { path, scope } = selectWritePath(flags);
272
+ mutateConfig(path, (config) => {
273
+ config.webSearch = { ...(config.webSearch ?? {}), enabled: action === "enable" };
274
+ });
275
+ console.log(`${action === "enable" ? "Enabled" : "Disabled"} agent_browser_web_search in ${scope} config: ${path}`);
276
+ return;
277
+ }
278
+ throw new UsageError(`Unsupported web-search action: ${action ?? ""}`);
279
+ }
280
+
281
+ function handleBrowser(args, flags) {
282
+ const target = args[0];
283
+ const action = args[1];
284
+ if (target === "profile") {
285
+ if (action === "status") {
286
+ printStatus();
287
+ return;
288
+ }
289
+ if (action === "set") {
290
+ const name = args.slice(2).join(" ").trim();
291
+ if (!name) throw new UsageError("browser profile set requires a profile name or profile directory path.");
292
+ const policy = flags.get("--policy") || "authenticated-only";
293
+ if (!["explicit-only", "authenticated-only", "always"].includes(policy)) throw new UsageError("Invalid --policy value.");
294
+ if (flags.get("--project") && policy !== "explicit-only") {
295
+ throw new UsageError("Project-local browser profile config may only use --policy explicit-only; authenticated or always profile guidance must be configured globally or through PI_AGENT_BROWSER_CONFIG.");
296
+ }
297
+ const { path, scope } = selectWritePath(flags);
298
+ mutateConfig(path, (config) => {
299
+ config.browser = { ...(config.browser ?? {}), defaultProfile: { name, policy } };
300
+ });
301
+ console.log(`Saved browser default profile in ${scope} config: ${path}`);
302
+ return;
303
+ }
304
+ if (action === "clear") {
305
+ const { path, scope } = selectWritePath(flags);
306
+ mutateConfig(path, (config) => {
307
+ if (config.browser) delete config.browser.defaultProfile;
308
+ });
309
+ console.log(`Cleared browser default profile in ${scope} config: ${path}`);
310
+ return;
311
+ }
312
+ throw new UsageError(`Unsupported browser profile action: ${action ?? ""}`);
313
+ }
314
+ if (target === "executable") {
315
+ if (action === "status") {
316
+ printStatus();
317
+ return;
318
+ }
319
+ if (action === "set") {
320
+ const executablePath = args.slice(2).join(" ").trim();
321
+ if (!executablePath) throw new UsageError("browser executable set requires a browser executable path.");
322
+ if (flags.get("--project")) {
323
+ throw new UsageError("Project-local browser executable config cannot steer host launch guidance; configure it globally or through PI_AGENT_BROWSER_CONFIG.");
324
+ }
325
+ const { path, scope } = selectWritePath(flags);
326
+ mutateConfig(path, (config) => {
327
+ config.browser = { ...(config.browser ?? {}), executablePath };
328
+ });
329
+ console.log(`Saved browser executable path in ${scope} config: ${path}`);
330
+ return;
331
+ }
332
+ if (action === "clear") {
333
+ const { path, scope } = selectWritePath(flags);
334
+ mutateConfig(path, (config) => {
335
+ if (config.browser) delete config.browser.executablePath;
336
+ });
337
+ console.log(`Cleared browser executable path in ${scope} config: ${path}`);
338
+ return;
339
+ }
340
+ throw new UsageError(`Unsupported browser executable action: ${action ?? ""}`);
341
+ }
342
+ throw new UsageError(`Unsupported browser action: ${target ?? ""}`);
343
+ }
344
+
345
+ export async function main(argv = process.argv.slice(2)) {
346
+ const { flags, positional } = parseArgs(argv);
347
+ if (flags.get("--help") || positional.length === 0) {
348
+ console.log(usage());
349
+ return 0;
350
+ }
351
+ const command = positional[0];
352
+ if (command === "paths") {
353
+ printPaths();
354
+ return 0;
355
+ }
356
+ if (command === "show") {
357
+ printStatus();
358
+ return 0;
359
+ }
360
+ if (command === "web-search") {
361
+ await handleWebSearch(positional.slice(1), flags);
362
+ return 0;
363
+ }
364
+ if (command === "browser") {
365
+ handleBrowser(positional.slice(1), flags);
366
+ return 0;
367
+ }
368
+ throw new UsageError(`Unknown command: ${command}`);
369
+ }
370
+
371
+ if (import.meta.url === `file://${process.argv[1]}`) {
372
+ main().catch((error) => {
373
+ if (error instanceof UsageError) {
374
+ console.error(error.message);
375
+ console.error(usage());
376
+ process.exit(2);
377
+ }
378
+ console.error(error instanceof Error ? error.message : String(error));
379
+ process.exit(1);
380
+ });
381
+ }