pi-agent-browser-native 0.2.40 → 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.
@@ -1,17 +1,33 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
3
  * Purpose: Manage pi-agent-browser-native package config under Pi-scoped config paths.
4
- * Responsibilities: Print config paths/status, write redacted web-search and browser profile settings, preserve safe permissions, and avoid echoing secrets.
5
- * Scope: Maintainer/user setup CLI only; extension runtime validation and tool execution live under extensions/agent-browser/lib/.
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
6
  */
7
7
 
8
8
  import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
9
- import { dirname, resolve } from "node:path";
9
+ import { dirname } from "node:path";
10
10
  import process from "node:process";
11
11
 
12
- const CONFIG_ENV = "PI_AGENT_BROWSER_CONFIG";
13
- const BRAVE_API_KEY_ENV = "BRAVE_API_KEY";
14
- const RELATIVE_CONFIG = [".pi", "config", "pi-agent-browser-native", "config.json"];
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
+
15
31
  const DEFAULT_CONFIG = { version: 1 };
16
32
 
17
33
  class UsageError extends Error {
@@ -28,45 +44,29 @@ Usage:
28
44
  pi-agent-browser-config paths
29
45
  pi-agent-browser-config show
30
46
  pi-agent-browser-config web-search status
31
- pi-agent-browser-config web-search set-key --stdin [--global]
32
- pi-agent-browser-config web-search set-env <ENV_VAR> [--global|--project]
33
- pi-agent-browser-config web-search set-command <command> [--global]
34
- pi-agent-browser-config web-search clear [--global|--project]
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]
35
54
  pi-agent-browser-config browser profile status
36
- pi-agent-browser-config browser profile set <name> [--policy explicit-only|authenticated-only|always] [--global|--project]
55
+ pi-agent-browser-config browser profile set <name|path> [--policy explicit-only|authenticated-only|always] [--global|--project]
37
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]
38
60
 
39
61
  Notes:
40
62
  Global config: ~/.pi/config/pi-agent-browser-native/config.json
41
63
  Project config: .pi/config/pi-agent-browser-native/config.json
42
- Override: PI_AGENT_BROWSER_CONFIG=/path/to/config.json
43
- Project-local plaintext, interpolation-literal, malformed, and command-backed web-search keys are refused; use exact set-env references there.
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}.
44
67
  `;
45
68
  }
46
69
 
47
- function getHome(env = process.env) {
48
- return env.HOME?.trim() || env.USERPROFILE?.trim();
49
- }
50
-
51
- function getGlobalConfigPath(env = process.env) {
52
- const home = getHome(env);
53
- if (!home) throw new Error("Could not resolve home directory for global config.");
54
- return resolve(home, ...RELATIVE_CONFIG);
55
- }
56
-
57
- function getProjectConfigPath(cwd = process.cwd()) {
58
- return resolve(cwd, ...RELATIVE_CONFIG);
59
- }
60
-
61
- function getPaths(env = process.env, cwd = process.cwd()) {
62
- const override = env[CONFIG_ENV]?.trim();
63
- return {
64
- global: getGlobalConfigPath(env),
65
- project: getProjectConfigPath(cwd),
66
- override: override ? resolve(override) : undefined,
67
- };
68
- }
69
-
70
70
  function parseArgs(argv) {
71
71
  const positional = [];
72
72
  const flags = new Map();
@@ -80,9 +80,9 @@ function parseArgs(argv) {
80
80
  flags.set(arg, true);
81
81
  continue;
82
82
  }
83
- if (arg === "--policy") {
83
+ if (arg === "--policy" || arg === "--provider") {
84
84
  const value = argv[index + 1];
85
- if (!value || value.startsWith("--")) throw new UsageError("--policy requires a value.");
85
+ if (!value || value.startsWith("--")) throw new UsageError(`${arg} requires a value.`);
86
86
  flags.set(arg, value);
87
87
  index += 1;
88
88
  continue;
@@ -112,57 +112,75 @@ function writeConfig(path, config) {
112
112
  }
113
113
 
114
114
  function selectWritePath(flags) {
115
- const paths = getPaths();
115
+ const paths = getAgentBrowserConfigPaths();
116
116
  if (flags.get("--project")) return { path: paths.project, scope: "project" };
117
117
  return { path: paths.global, scope: "global" };
118
118
  }
119
119
 
120
- function classifyCredential(rawValue) {
121
- const trimmed = String(rawValue ?? "").trim();
122
- if (!trimmed) return "not configured";
123
- if (trimmed.startsWith("!")) return "configured via command";
124
- if (trimmed.includes("$")) return "configured via environment interpolation";
125
- return "configured as plaintext [redacted]";
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(", ")}.`);
126
123
  }
127
124
 
128
- function mergeConfig() {
129
- const paths = getPaths();
130
- const layers = [];
131
- for (const [scope, path] of [["global", paths.global], ["project", paths.project], ...(paths.override ? [["override", paths.override]] : [])]) {
132
- if (!existsSync(path)) continue;
133
- layers.push({ scope, path, config: readConfig(path) });
125
+ function inferWebSearchProviderFromEnvName(envName) {
126
+ for (const provider of WEB_SEARCH_PROVIDERS) {
127
+ if (envName === getWebSearchProviderEnvVar(provider)) return provider;
134
128
  }
135
- const merged = layers.reduce((current, layer) => ({
136
- ...current,
137
- ...layer.config,
138
- browser: { ...(current.browser ?? {}), ...(layer.config.browser ?? {}) },
139
- webSearch: { ...(current.webSearch ?? {}), ...(layer.config.webSearch ?? {}) },
140
- }), { ...DEFAULT_CONFIG });
141
- return { layers, merged, paths };
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)];
142
146
  }
143
147
 
144
148
  function printPaths() {
145
- const paths = getPaths();
149
+ const paths = getAgentBrowserConfigPaths();
146
150
  console.log(`Global: ${paths.global}`);
147
151
  console.log(`Project: ${paths.project}`);
148
- console.log(`Override: ${paths.override ?? `${CONFIG_ENV} not set`}`);
152
+ console.log(`Override: ${paths.override ?? `${AGENT_BROWSER_CONFIG_ENV} not set`}`);
149
153
  }
150
154
 
151
155
  function printStatus() {
152
- const { layers, merged, paths } = mergeConfig();
156
+ const state = loadAgentBrowserConfigStateSync({ cwd: process.cwd(), env: process.env });
153
157
  printPaths();
154
158
  console.log("");
155
159
  console.log("Config files:");
156
- for (const [scope, path] of [["global", paths.global], ["project", paths.project], ...(paths.override ? [["override", paths.override]] : [])]) {
157
- console.log(` ${scope}: ${path} ${existsSync(path) ? "[exists]" : "[missing]"}`);
160
+ for (const file of summarizeConfigFiles(state)) {
161
+ console.log(` ${file.scope}: ${file.path} ${file.exists ? "[exists]" : "[missing]"}`);
158
162
  }
159
163
  console.log("");
160
164
  console.log("Effective config:");
161
- const source = merged.webSearch?.braveApiKey;
162
- console.log(` webSearch.braveApiKey: ${source ? classifyCredential(source) : process.env[BRAVE_API_KEY_ENV]?.trim() ? `configured via ${BRAVE_API_KEY_ENV} environment fallback` : "not configured"}`);
163
- const profile = merged.browser?.defaultProfile;
164
- console.log(` browser.defaultProfile: ${profile?.name ? `${profile.name} (policy: ${profile.policy ?? "authenticated-only"})` : "not configured"}`);
165
- if (layers.length === 0) console.log(" layers: none");
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
+ }
166
184
  }
167
185
 
168
186
  async function readSecretFromStdin(useStdin) {
@@ -187,75 +205,141 @@ async function handleWebSearch(args, flags) {
187
205
  return;
188
206
  }
189
207
  if (action === "set-key") {
190
- if (flags.get("--project")) throw new UsageError("Plaintext Brave keys cannot be written to project-local config. Use set-env or set-command.");
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.`);
191
210
  const key = await readSecretFromStdin(Boolean(flags.get("--stdin")));
192
211
  const { path } = selectWritePath(flags);
193
212
  mutateConfig(path, (config) => {
194
- config.webSearch = { ...(config.webSearch ?? {}), braveApiKey: key };
213
+ setWebSearchCredential(config, provider, key);
195
214
  });
196
- console.log(`Saved Brave Search key to global config: ${path}`);
215
+ console.log(`Saved ${getWebSearchProviderLabel(provider)} key to global config: ${path}`);
197
216
  return;
198
217
  }
199
218
  if (action === "set-env") {
200
219
  const envName = args[1];
201
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
+ }
202
226
  const { path, scope } = selectWritePath(flags);
203
227
  mutateConfig(path, (config) => {
204
- config.webSearch = { ...(config.webSearch ?? {}), braveApiKey: `$${envName}` };
228
+ setWebSearchCredential(config, provider, envReference);
205
229
  });
206
- console.log(`Saved Brave Search ${scope} env reference to: ${path}`);
230
+ console.log(`Saved ${getWebSearchProviderLabel(provider)} ${scope} env reference to: ${path}`);
207
231
  return;
208
232
  }
209
233
  if (action === "set-command") {
210
- if (flags.get("--project")) throw new UsageError("Command-backed Brave keys cannot be written to project-local config. Use set-env there.");
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.`);
211
236
  const command = args.slice(1).join(" ").trim();
212
237
  if (!command) throw new UsageError("set-command requires a command string.");
213
238
  const { path, scope } = selectWritePath(flags);
214
239
  mutateConfig(path, (config) => {
215
- config.webSearch = { ...(config.webSearch ?? {}), braveApiKey: `!${command}` };
240
+ setWebSearchCredential(config, provider, `!${command}`);
216
241
  });
217
- console.log(`Saved Brave Search ${scope} command source to: ${path}`);
242
+ console.log(`Saved ${getWebSearchProviderLabel(provider)} ${scope} command source to: ${path}`);
218
243
  return;
219
244
  }
220
245
  if (action === "clear") {
246
+ const provider = getWebSearchProvider(flags, { allowAll: true });
221
247
  const { path, scope } = selectWritePath(flags);
222
248
  mutateConfig(path, (config) => {
223
- if (config.webSearch) delete config.webSearch.braveApiKey;
249
+ if (provider === "all") {
250
+ for (const entry of WEB_SEARCH_PROVIDERS) clearWebSearchCredential(config, entry);
251
+ } else {
252
+ clearWebSearchCredential(config, provider);
253
+ }
224
254
  });
225
- console.log(`Cleared Brave Search credential source in ${scope} config: ${path}`);
226
- return;
227
- }
228
- throw new UsageError(`Unsupported web-search action: ${action ?? ""}`);
229
- }
230
-
231
- function handleBrowser(args, flags) {
232
- if (args[0] !== "profile") throw new UsageError(`Unsupported browser action: ${args[0] ?? ""}`);
233
- const action = args[1];
234
- if (action === "status") {
235
- printStatus();
255
+ console.log(`Cleared ${provider === "all" ? "all web-search" : getWebSearchProviderLabel(provider)} credential source in ${scope} config: ${path}`);
236
256
  return;
237
257
  }
238
- if (action === "set") {
239
- const name = args[2]?.trim();
240
- if (!name) throw new UsageError("browser profile set requires a profile name.");
241
- const policy = flags.get("--policy") || "authenticated-only";
242
- if (!["explicit-only", "authenticated-only", "always"].includes(policy)) throw new UsageError("Invalid --policy value.");
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.");
243
261
  const { path, scope } = selectWritePath(flags);
244
262
  mutateConfig(path, (config) => {
245
- config.browser = { ...(config.browser ?? {}), defaultProfile: { name, policy } };
263
+ config.webSearch = { ...(config.webSearch ?? {}) };
264
+ if (provider === "auto") delete config.webSearch.preferredProvider;
265
+ else config.webSearch.preferredProvider = provider;
246
266
  });
247
- console.log(`Saved browser default profile in ${scope} config: ${path}`);
267
+ console.log(`${provider === "auto" ? "Cleared" : "Saved"} web-search preferred provider in ${scope} config: ${path}`);
248
268
  return;
249
269
  }
250
- if (action === "clear") {
270
+ if (action === "enable" || action === "disable") {
251
271
  const { path, scope } = selectWritePath(flags);
252
272
  mutateConfig(path, (config) => {
253
- if (config.browser) delete config.browser.defaultProfile;
273
+ config.webSearch = { ...(config.webSearch ?? {}), enabled: action === "enable" };
254
274
  });
255
- console.log(`Cleared browser default profile in ${scope} config: ${path}`);
275
+ console.log(`${action === "enable" ? "Enabled" : "Disabled"} agent_browser_web_search in ${scope} config: ${path}`);
256
276
  return;
257
277
  }
258
- throw new UsageError(`Unsupported browser profile action: ${action ?? ""}`);
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 ?? ""}`);
259
343
  }
260
344
 
261
345
  export async function main(argv = process.argv.slice(2)) {