@demigodmode/pi-web-agent 0.4.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -18,6 +18,10 @@ That sounds obvious, but a lot of agent tooling gets fuzzy right there. This pac
18
18
  pi install npm:@demigodmode/pi-web-agent
19
19
  ```
20
20
 
21
+ After installing, reload or restart Pi. Run `/web-agent` for the action menu, or `/web-agent doctor` to check whether the package loaded cleanly and whether headless rendering can find a browser.
22
+
23
+ Headless rendering currently requires a detectable Chromium-family browser: Chrome, Chromium, Edge, or Brave. Firefox/Safari-only systems can still use search and plain HTTP reads, but browser-rendered fallback pages need a supported Chromium-family browser for now.
24
+
21
25
  Later on, update installed packages with:
22
26
 
23
27
  ```bash
@@ -65,6 +69,7 @@ Primary UI:
65
69
  Helper commands:
66
70
 
67
71
  ```text
72
+ /web-agent doctor
68
73
  /web-agent show
69
74
  /web-agent reset project
70
75
  /web-agent reset global
@@ -1,10 +1,18 @@
1
1
  import { type ExtensionAPI } from '@mariozechner/pi-coding-agent';
2
2
  import { loadPresentationConfigLayers, type LoadedPresentationConfig } from '../presentation/config-store.js';
3
+ import { type BrowserResolutionResult } from '../fetch/browser-resolution.js';
3
4
  import type { PresentationConfig, PresentationConfigOverride, PresentationScope } from '../presentation/types.js';
4
5
  type CommandDeps = {
5
6
  load?: () => ReturnType<typeof loadPresentationConfigLayers>;
6
7
  save?: (scope: PresentationScope, config: PresentationConfigOverride) => Promise<void>;
7
8
  reset?: (scope: PresentationScope) => Promise<void>;
9
+ resolveBrowser?: () => Promise<BrowserResolutionResult>;
10
+ runtime?: {
11
+ nodeVersion: string;
12
+ platform: string;
13
+ arch: string;
14
+ };
15
+ checkTypebox?: () => Promise<boolean>;
8
16
  };
9
17
  export type SettingsDraftState = {
10
18
  scope: PresentationScope;
@@ -1,7 +1,8 @@
1
- import { getSettingsListTheme } from '@mariozechner/pi-coding-agent';
2
- import { Container, SettingsList, Text } from '@mariozechner/pi-tui';
1
+ import { DynamicBorder, getSettingsListTheme } from '@mariozechner/pi-coding-agent';
2
+ import { Container, SelectList, SettingsList, Text } from '@mariozechner/pi-tui';
3
3
  import { DEFAULT_PRESENTATION_CONFIG, mergePresentationConfigLayers, resolvePresentationMode } from '../presentation/config.js';
4
4
  import { loadPresentationConfigLayers, resetPresentationConfigScope, savePresentationConfigScope } from '../presentation/config-store.js';
5
+ import { resolveBrowserExecutable } from '../fetch/browser-resolution.js';
5
6
  const PRESENTATION_TOOL_NAMES = ['web_explore'];
6
7
  function parseScopeToken(token) {
7
8
  return token === 'global' || token === 'project' ? token : undefined;
@@ -12,6 +13,15 @@ function clonePresentationConfig(config) {
12
13
  tools: { ...config.tools }
13
14
  };
14
15
  }
16
+ async function defaultCheckTypebox() {
17
+ try {
18
+ await import('typebox');
19
+ return true;
20
+ }
21
+ catch {
22
+ return false;
23
+ }
24
+ }
15
25
  function formatConfigSummary(config) {
16
26
  const lines = [`defaultMode: ${config.defaultMode}`];
17
27
  for (const toolName of PRESENTATION_TOOL_NAMES) {
@@ -137,6 +147,40 @@ export function handleSettingsShortcut(data) {
137
147
  }
138
148
  return undefined;
139
149
  }
150
+ async function openActionMenu(ctx) {
151
+ return ctx.ui.custom((tui, theme, _kb, done) => {
152
+ const container = new Container();
153
+ const items = [
154
+ { value: 'settings', label: 'Settings', description: 'Edit presentation modes' },
155
+ { value: 'show', label: 'Show config', description: 'Print effective config paths and modes' },
156
+ { value: 'doctor', label: 'Doctor', description: 'Check runtime dependencies and browser detection' },
157
+ { value: 'reset-project', label: 'Reset project config', description: 'Delete project-level overrides' },
158
+ { value: 'reset-global', label: 'Reset global config', description: 'Delete global overrides' }
159
+ ];
160
+ container.addChild(new DynamicBorder((text) => theme.fg('accent', text)));
161
+ container.addChild(new Text(theme.fg('accent', theme.bold('pi-web-agent')), 1, 0));
162
+ const list = new SelectList(items, Math.min(items.length, 8), {
163
+ selectedPrefix: (text) => theme.fg('accent', text),
164
+ selectedText: (text) => theme.fg('accent', text),
165
+ description: (text) => theme.fg('muted', text),
166
+ scrollInfo: (text) => theme.fg('dim', text),
167
+ noMatch: (text) => theme.fg('warning', text)
168
+ });
169
+ list.onSelect = (item) => done(item.value);
170
+ list.onCancel = () => done(undefined);
171
+ container.addChild(list);
172
+ container.addChild(new Text(theme.fg('dim', '↑↓ navigate • enter select • esc cancel'), 1, 0));
173
+ container.addChild(new DynamicBorder((text) => theme.fg('accent', text)));
174
+ return {
175
+ render: (width) => container.render(width),
176
+ invalidate: () => container.invalidate(),
177
+ handleInput: (data) => {
178
+ list.handleInput?.(data);
179
+ tui.requestRender?.();
180
+ }
181
+ };
182
+ });
183
+ }
140
184
  async function openSettingsUi(ctx, loaded, initialScope) {
141
185
  return ctx.ui.custom((_tui, theme, _kb, done) => {
142
186
  let state = createSettingsDraftState(loaded, initialScope);
@@ -182,10 +226,51 @@ export function registerWebAgentConfigCommands(pi, deps = {}) {
182
226
  const load = deps.load ?? (() => loadPresentationConfigLayers());
183
227
  const save = deps.save ?? ((scope, config) => savePresentationConfigScope({}, scope, config));
184
228
  const reset = deps.reset ?? ((scope) => resetPresentationConfigScope({}, scope));
229
+ const resolveBrowser = deps.resolveBrowser ?? (() => resolveBrowserExecutable({}));
230
+ const runtime = deps.runtime ?? {
231
+ nodeVersion: process.version,
232
+ platform: process.platform,
233
+ arch: process.arch
234
+ };
235
+ const checkTypebox = deps.checkTypebox ?? defaultCheckTypebox;
185
236
  pi.registerCommand('web-agent', {
186
237
  description: 'Open settings or manage pi-web-agent presentation config',
187
238
  handler: async (args, ctx) => {
188
- const [action, maybeScope] = (args ?? '').trim().split(/\s+/).filter(Boolean);
239
+ let [action, maybeScope] = (args ?? '').trim().split(/\s+/).filter(Boolean);
240
+ if (!action) {
241
+ const selectedAction = await openActionMenu(ctx);
242
+ if (!selectedAction)
243
+ return;
244
+ if (selectedAction === 'reset-project') {
245
+ action = 'reset';
246
+ maybeScope = 'project';
247
+ }
248
+ else if (selectedAction === 'reset-global') {
249
+ action = 'reset';
250
+ maybeScope = 'global';
251
+ }
252
+ else {
253
+ action = selectedAction;
254
+ }
255
+ }
256
+ if (action === 'doctor') {
257
+ const [typeboxOk, browser] = await Promise.all([checkTypebox(), resolveBrowser()]);
258
+ const lines = [
259
+ 'pi-web-agent: loaded',
260
+ `runtime: node ${runtime.nodeVersion} ${runtime.platform} ${runtime.arch}`,
261
+ `typebox: ${typeboxOk ? 'ok' : 'missing'}`
262
+ ];
263
+ if (browser.ok) {
264
+ lines.push(`browser: ${browser.browser} ${browser.executablePath}`);
265
+ }
266
+ else {
267
+ lines.push(`browser: missing (${browser.error.code})`);
268
+ lines.push(browser.error.message);
269
+ lines.push('Install Chrome, Chromium, Edge, or Brave and run /web-agent doctor again.');
270
+ }
271
+ ctx.ui.notify(lines.join('\n'), 'info');
272
+ return;
273
+ }
189
274
  if (action === 'show') {
190
275
  const loaded = await load();
191
276
  ctx.ui.notify([
@@ -243,7 +328,7 @@ export function registerWebAgentConfigCommands(pi, deps = {}) {
243
328
  ctx.ui.notify(`Saved ${result.scope} config`, 'info');
244
329
  return;
245
330
  }
246
- ctx.ui.notify('Use /web-agent, /web-agent show, /web-agent reset project, or /web-agent settings', 'info');
331
+ ctx.ui.notify('Use /web-agent, /web-agent show, /web-agent doctor, /web-agent reset project, or /web-agent settings', 'info');
247
332
  }
248
333
  });
249
334
  }
package/dist/extension.js CHANGED
@@ -1,4 +1,4 @@
1
- import { Type } from '@sinclair/typebox';
1
+ import { Type } from 'typebox';
2
2
  import { registerWebAgentConfigCommands } from './commands/web-agent-config.js';
3
3
  import { DEFAULT_PRESENTATION_CONFIG, resolvePresentationMode } from './presentation/config.js';
4
4
  import { loadPresentationConfigLayers } from './presentation/config-store.js';
@@ -1,7 +1,9 @@
1
+ type SupportedPlatform = NodeJS.Platform | string;
2
+ type BrowserName = 'configured' | 'chrome' | 'edge' | 'brave' | 'chromium';
1
3
  export type BrowserResolutionResult = {
2
4
  ok: true;
3
5
  executablePath: string;
4
- browser: 'configured' | 'chrome' | 'edge';
6
+ browser: BrowserName;
5
7
  } | {
6
8
  ok: false;
7
9
  error: {
@@ -9,7 +11,10 @@ export type BrowserResolutionResult = {
9
11
  message: string;
10
12
  };
11
13
  };
12
- export declare function resolveBrowserExecutable({ configuredPath, fileExists }: {
14
+ export declare function resolveBrowserExecutable({ configuredPath, platform, env, fileExists }: {
13
15
  configuredPath?: string;
16
+ platform?: SupportedPlatform;
17
+ env?: NodeJS.ProcessEnv | Record<string, string | undefined>;
14
18
  fileExists?: (path: string) => Promise<boolean>;
15
19
  }): Promise<BrowserResolutionResult>;
20
+ export {};
@@ -1,14 +1,82 @@
1
- const WINDOWS_CANDIDATES = {
2
- chrome: [
3
- 'C:/Program Files/Google/Chrome/Application/chrome.exe',
4
- 'C:/Program Files (x86)/Google/Chrome/Application/chrome.exe'
5
- ],
6
- edge: [
7
- 'C:/Program Files/Microsoft/Edge/Application/msedge.exe',
8
- 'C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe'
9
- ]
10
- };
11
- export async function resolveBrowserExecutable({ configuredPath, fileExists = defaultFileExists }) {
1
+ const WINDOWS_CANDIDATES = [
2
+ {
3
+ browser: 'chrome',
4
+ paths: [
5
+ 'C:/Program Files/Google/Chrome/Application/chrome.exe',
6
+ 'C:/Program Files (x86)/Google/Chrome/Application/chrome.exe'
7
+ ],
8
+ commands: ['chrome.exe', 'chrome', 'google-chrome']
9
+ },
10
+ {
11
+ browser: 'edge',
12
+ paths: [
13
+ 'C:/Program Files/Microsoft/Edge/Application/msedge.exe',
14
+ 'C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe'
15
+ ],
16
+ commands: ['msedge.exe', 'msedge', 'microsoft-edge']
17
+ },
18
+ {
19
+ browser: 'brave',
20
+ paths: [
21
+ 'C:/Program Files/BraveSoftware/Brave-Browser/Application/brave.exe',
22
+ 'C:/Program Files (x86)/BraveSoftware/Brave-Browser/Application/brave.exe'
23
+ ],
24
+ commands: ['brave.exe', 'brave']
25
+ },
26
+ {
27
+ browser: 'chromium',
28
+ paths: [
29
+ 'C:/Program Files/Chromium/Application/chrome.exe',
30
+ 'C:/Program Files (x86)/Chromium/Application/chrome.exe'
31
+ ],
32
+ commands: ['chromium.exe', 'chromium']
33
+ }
34
+ ];
35
+ const MACOS_CANDIDATES = [
36
+ {
37
+ browser: 'chrome',
38
+ paths: ['/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'],
39
+ commands: ['google-chrome', 'chrome']
40
+ },
41
+ {
42
+ browser: 'edge',
43
+ paths: ['/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge'],
44
+ commands: ['microsoft-edge', 'msedge']
45
+ },
46
+ {
47
+ browser: 'brave',
48
+ paths: ['/Applications/Brave Browser.app/Contents/MacOS/Brave Browser'],
49
+ commands: ['brave-browser', 'brave']
50
+ },
51
+ {
52
+ browser: 'chromium',
53
+ paths: ['/Applications/Chromium.app/Contents/MacOS/Chromium'],
54
+ commands: ['chromium', 'chromium-browser']
55
+ }
56
+ ];
57
+ const LINUX_CANDIDATES = [
58
+ {
59
+ browser: 'chrome',
60
+ paths: ['/usr/bin/google-chrome', '/usr/local/bin/google-chrome', '/opt/google/chrome/chrome'],
61
+ commands: ['google-chrome', 'google-chrome-stable', 'chrome']
62
+ },
63
+ {
64
+ browser: 'edge',
65
+ paths: ['/usr/bin/microsoft-edge', '/opt/microsoft/msedge/msedge'],
66
+ commands: ['microsoft-edge', 'microsoft-edge-stable', 'msedge']
67
+ },
68
+ {
69
+ browser: 'brave',
70
+ paths: ['/usr/bin/brave-browser', '/usr/local/bin/brave-browser', '/opt/brave.com/brave/brave'],
71
+ commands: ['brave-browser', 'brave']
72
+ },
73
+ {
74
+ browser: 'chromium',
75
+ paths: ['/usr/bin/chromium', '/usr/bin/chromium-browser', '/snap/bin/chromium'],
76
+ commands: ['chromium', 'chromium-browser']
77
+ }
78
+ ];
79
+ export async function resolveBrowserExecutable({ configuredPath, platform = process.platform, env = process.env, fileExists = defaultFileExists }) {
12
80
  if (configuredPath) {
13
81
  if (await fileExists(configuredPath)) {
14
82
  return {
@@ -25,14 +93,19 @@ export async function resolveBrowserExecutable({ configuredPath, fileExists = de
25
93
  }
26
94
  };
27
95
  }
28
- for (const path of WINDOWS_CANDIDATES.chrome) {
29
- if (await fileExists(path)) {
30
- return { ok: true, executablePath: path, browser: 'chrome' };
96
+ const candidates = getCandidatesForPlatform(platform);
97
+ for (const candidate of candidates) {
98
+ for (const path of candidate.paths) {
99
+ if (await fileExists(path)) {
100
+ return { ok: true, executablePath: path, browser: candidate.browser };
101
+ }
31
102
  }
32
103
  }
33
- for (const path of WINDOWS_CANDIDATES.edge) {
34
- if (await fileExists(path)) {
35
- return { ok: true, executablePath: path, browser: 'edge' };
104
+ for (const candidate of candidates) {
105
+ for (const path of getPathCommandCandidates(candidate.commands, platform, env)) {
106
+ if (await fileExists(path)) {
107
+ return { ok: true, executablePath: path, browser: candidate.browser };
108
+ }
36
109
  }
37
110
  }
38
111
  return {
@@ -43,6 +116,27 @@ export async function resolveBrowserExecutable({ configuredPath, fileExists = de
43
116
  }
44
117
  };
45
118
  }
119
+ function getCandidatesForPlatform(platform) {
120
+ if (platform === 'win32')
121
+ return WINDOWS_CANDIDATES;
122
+ if (platform === 'darwin')
123
+ return MACOS_CANDIDATES;
124
+ if (platform === 'linux')
125
+ return LINUX_CANDIDATES;
126
+ return [...WINDOWS_CANDIDATES, ...MACOS_CANDIDATES, ...LINUX_CANDIDATES];
127
+ }
128
+ function getPathCommandCandidates(commands, platform, env) {
129
+ const pathValue = env.PATH ?? env.Path ?? env.path;
130
+ if (!pathValue)
131
+ return [];
132
+ const delimiter = platform === 'win32' ? ';' : ':';
133
+ const dirs = pathValue.split(delimiter).filter(Boolean);
134
+ const extensions = platform === 'win32' ? ['', '.exe'] : [''];
135
+ return dirs.flatMap((dir) => commands.flatMap((command) => extensions.map((extension) => {
136
+ const normalizedCommand = command.toLowerCase().endsWith(extension) ? command : `${command}${extension}`;
137
+ return `${dir.replace(/[\\/]$/, '')}/${normalizedCommand}`;
138
+ })));
139
+ }
46
140
  async function defaultFileExists(path) {
47
141
  try {
48
142
  const { access } = await import('node:fs/promises');
package/dist/types.d.ts CHANGED
@@ -19,7 +19,7 @@ export type FetchMetadata = {
19
19
  cacheHit: boolean;
20
20
  contentType?: string;
21
21
  truncated?: boolean;
22
- browser?: 'configured' | 'chrome' | 'edge';
22
+ browser?: 'configured' | 'chrome' | 'edge' | 'brave' | 'chromium';
23
23
  navigationMs?: number;
24
24
  };
25
25
  export type ExtractedContent = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@demigodmode/pi-web-agent",
3
- "version": "0.4.0",
3
+ "version": "0.5.1",
4
4
  "description": "Pi package for reliable web access with explicit search, fetch, and headless boundaries.",
5
5
  "type": "module",
6
6
  "main": "./dist/extension.js",
@@ -56,11 +56,11 @@
56
56
  "@mozilla/readability": "^0.6.0",
57
57
  "cheerio": "^1.1.0",
58
58
  "jsdom": "^26.0.0",
59
- "playwright-core": "^1.54.0"
59
+ "playwright-core": "^1.54.0",
60
+ "typebox": "^1.1.37"
60
61
  },
61
62
  "devDependencies": {
62
- "@mariozechner/pi-coding-agent": "^0.67.2",
63
- "@sinclair/typebox": "^0.34.41",
63
+ "@mariozechner/pi-coding-agent": "^0.69.0",
64
64
  "@types/jsdom": "^21.1.7",
65
65
  "@types/node": "^24.0.0",
66
66
  "@vitest/coverage-v8": "^3.2.4",
@@ -69,7 +69,6 @@
69
69
  "vitest": "^3.2.0"
70
70
  },
71
71
  "peerDependencies": {
72
- "@mariozechner/pi-coding-agent": "*",
73
- "@sinclair/typebox": "*"
72
+ "@mariozechner/pi-coding-agent": "*"
74
73
  }
75
74
  }