@demigodmode/pi-web-agent 1.6.0 → 1.6.2

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 CHANGED
@@ -18,9 +18,38 @@ The format is intentionally simple and release-oriented.
18
18
  ### Breaking
19
19
  - None.
20
20
 
21
+ ## [1.6.2] - 2026-07-10
22
+ ### Added
23
+ - None.
24
+
25
+ ### Changed
26
+ - None.
27
+
28
+ ### Fixed
29
+ - Extension load failure on install (`Cannot find module 'punycode/'` / `Set operation called on non-Set object`) caused by pi's extension loader mishandling a couple of patterns in jsdom's dependency tree (tr46, cssstyle). Added a postinstall step that patches the affected files. Fixes #34
30
+
31
+ ### Breaking
32
+ - None.
33
+
34
+ ## [1.6.1] - 2026-07-05
35
+ ### Added
36
+ - None.
37
+
38
+ ### Changed
39
+ - None.
40
+
41
+ ### Fixed
42
+ - Setting a SearXNG URL from **Settings → Backends** now actually saves, and switches the search provider to `searxng` if it wasn't already selected. Previously the URL was silently discarded unless SearXNG was already the active provider. Reported by @Josephur in #33.
43
+ - Same fix applied on the fetch side: setting a Firecrawl URL now switches the fetch provider to `firecrawl` too, instead of leaving it stuck on `http` until you flip it manually.
44
+ - Editing the SearXNG/Firecrawl base URL no longer opens a separate nested input prompt on top of the settings list (which could leave the session unresponsive to further input). It's now an inline field in the same list, and pressing Esc there only cancels the field instead of closing the whole settings screen and discarding unrelated pending changes.
45
+ - The inline URL field now starts with the cursor at the end of the pre-filled value, so typing right away appends instead of inserting at the front of the string.
46
+
47
+ ### Breaking
48
+ - None.
49
+
21
50
  ## [1.6.0] - 2026-07-05
22
51
  ### Added
23
- - You.com is now available as a hosted search backend for `web_explore`, selectable alongside DuckDuckGo, SearXNG, and Brave. Set `YDC_API_KEY` in the environment and choose `youcom` from **Settings → Backends**. You.com handles source discovery only; `web_explore` still fetches pages, ranks evidence, handles caveats, and synthesizes the answer itself.
52
+ - You.com is now available as a hosted search backend for `web_explore`, selectable alongside DuckDuckGo, SearXNG, and Brave. Set `YDC_API_KEY` in the environment and choose `youcom` from **Settings → Backends**. You.com handles source discovery only; `web_explore` still fetches pages, ranks evidence, handles caveats, and synthesizes the answer itself. Contributed by @ydc-oss-bot in #32 - first outside PR this project has had.
24
53
  - `/web-agent settings` now treats You.com as a first-class search backend (with optional DuckDuckGo fallback) while keeping API keys out of config files via `YDC_API_KEY`.
25
54
  - `/web-agent doctor` now reports You.com setup status, warns when `YDC_API_KEY` is missing, and validates configured You.com access when a key is present.
26
55
 
package/README.md CHANGED
@@ -111,6 +111,8 @@ Example:
111
111
 
112
112
  Backend config is also supported. Defaults remain DuckDuckGo search, plain HTTP fetch, and local-browser headless fallback with managed Chromium fallback configured. If you have a Brave Search or You.com API key, either can be selected as a hosted search backend while `web_explore` still handles page reading, ranking, and caveats itself.
113
113
 
114
+ Search backends: DuckDuckGo (default), SearXNG (self-hosted), Brave Search (hosted), You.com (hosted).
115
+
114
116
  Backend settings can be changed from:
115
117
 
116
118
  ```text
@@ -1,5 +1,6 @@
1
1
  import { type BackendConfig, type BackendConfigOverride } from '../backends/config.js';
2
2
  import { type ExtensionAPI } from '@earendil-works/pi-coding-agent';
3
+ import { type Component } from '@earendil-works/pi-tui';
3
4
  import { loadPresentationConfigLayers, type LoadedPresentationConfig } from '../presentation/config-store.js';
4
5
  import { type BrowserResolutionResult } from '../fetch/browser-resolution.js';
5
6
  import type { PresentationConfig, PresentationConfigOverride, PresentationScope } from '../presentation/types.js';
@@ -32,6 +33,7 @@ export declare function validateBackendUrl(value: string): {
32
33
  ok: false;
33
34
  message: string;
34
35
  };
36
+ export declare function createBackendUrlEditor(theme: any, label: string, placeholderUrl: string, onOpenChange?: (open: boolean) => void): (currentValue: string, done: (selectedValue?: string) => void) => Component;
35
37
  export declare function getInheritedConfigForScope(loaded: Awaited<LoadedPresentationConfig>, scope: PresentationScope): PresentationConfig;
36
38
  export declare function getScopeDisplayConfig(loaded: Awaited<LoadedPresentationConfig>, scope: PresentationScope): PresentationConfig;
37
39
  export declare function getInheritedBackendsForScope(loaded: Awaited<LoadedPresentationConfig>, scope: PresentationScope): BackendConfig;
@@ -1,7 +1,7 @@
1
1
  import { DEFAULT_BACKEND_CONFIG, mergeBackendConfigLayers, validateBackendConfig } from '../backends/config.js';
2
2
  import { checkBackendHealth } from '../backends/doctor.js';
3
3
  import { DynamicBorder, getSettingsListTheme } from '@earendil-works/pi-coding-agent';
4
- import { Container, SelectList, SettingsList, Text } from '@earendil-works/pi-tui';
4
+ import { Container, Input, SelectList, SettingsList, Text } from '@earendil-works/pi-tui';
5
5
  import { DEFAULT_PRESENTATION_CONFIG, mergePresentationConfigLayers, resolvePresentationMode } from '../presentation/config.js';
6
6
  import { loadPresentationConfigLayers, resetPresentationConfigScope, saveBackendConfigScope, savePresentationConfigScope } from '../presentation/config-store.js';
7
7
  import { resolveBrowserExecutable } from '../fetch/browser-resolution.js';
@@ -112,7 +112,49 @@ function buildPresentationSettingsItems(scope, config) {
112
112
  }))
113
113
  ];
114
114
  }
115
- function buildBackendSettingsItems(scope, backends) {
115
+ export function createBackendUrlEditor(theme, label, placeholderUrl, onOpenChange) {
116
+ return (currentValue, done) => {
117
+ onOpenChange?.(true);
118
+ const initialValue = currentValue && currentValue !== 'not set' ? currentValue : placeholderUrl;
119
+ const container = new Container();
120
+ const hint = new Text(theme.fg('muted', `${label} · enter to save · empty clears · esc cancels`), 1, 0);
121
+ const input = new Input();
122
+ input.setValue(initialValue);
123
+ input.handleInput('\x1b[F'); // move cursor to end; setValue leaves it at 0
124
+ input.focused = true;
125
+ const errorText = new Text('', 1, 0);
126
+ const finish = (selectedValue) => {
127
+ onOpenChange?.(false);
128
+ done(selectedValue);
129
+ };
130
+ const showError = (message) => {
131
+ errorText.setText(theme.fg('warning', message));
132
+ container.invalidate();
133
+ };
134
+ input.onSubmit = (value) => {
135
+ if (!value.trim()) {
136
+ finish('');
137
+ return;
138
+ }
139
+ const validated = validateBackendUrl(value);
140
+ if (!validated.ok) {
141
+ showError(validated.message);
142
+ return;
143
+ }
144
+ finish(validated.value);
145
+ };
146
+ input.onEscape = () => finish(undefined);
147
+ container.addChild(hint);
148
+ container.addChild(input);
149
+ container.addChild(errorText);
150
+ return {
151
+ render: (width) => container.render(width),
152
+ invalidate: () => container.invalidate(),
153
+ handleInput: (data) => input.handleInput(data)
154
+ };
155
+ };
156
+ }
157
+ function buildBackendSettingsItems(scope, backends, theme, onUrlEditorOpenChange) {
116
158
  return [
117
159
  {
118
160
  id: 'scope',
@@ -130,7 +172,7 @@ function buildBackendSettingsItems(scope, backends) {
130
172
  id: 'backend:search:baseUrl',
131
173
  label: 'SearXNG URL',
132
174
  currentValue: backends.search.baseUrl ?? 'not set',
133
- values: ['edit']
175
+ submenu: createBackendUrlEditor(theme, 'SearXNG base URL', 'http://localhost:8080', onUrlEditorOpenChange)
134
176
  },
135
177
  {
136
178
  id: 'backend:search:fallback',
@@ -160,7 +202,7 @@ function buildBackendSettingsItems(scope, backends) {
160
202
  id: 'backend:fetch:baseUrl',
161
203
  label: 'Firecrawl URL',
162
204
  currentValue: backends.fetch.baseUrl ?? 'not set',
163
- values: ['edit']
205
+ submenu: createBackendUrlEditor(theme, 'Firecrawl base URL', 'http://localhost:3002', onUrlEditorOpenChange)
164
206
  },
165
207
  {
166
208
  id: 'backend:fetch:fallback',
@@ -280,7 +322,8 @@ export function applySettingsValue(state, id, newValue) {
280
322
  }
281
323
  }
282
324
  if (id === 'backend:search:baseUrl') {
283
- if (currentBackends.search.provider === 'searxng' && newValue.trim()) {
325
+ if (newValue.trim()) {
326
+ currentBackends.search.provider = 'searxng';
284
327
  currentBackends.search.baseUrl = newValue.trim();
285
328
  }
286
329
  else {
@@ -303,6 +346,7 @@ export function applySettingsValue(state, id, newValue) {
303
346
  }
304
347
  if (id === 'backend:fetch:baseUrl') {
305
348
  if (newValue.trim()) {
349
+ currentBackends.fetch.provider = 'firecrawl';
306
350
  currentBackends.fetch.baseUrl = newValue.trim();
307
351
  }
308
352
  else {
@@ -494,43 +538,20 @@ async function openPresentationSettingsUi(ctx, loaded, initialScope) {
494
538
  });
495
539
  }
496
540
  async function openBackendSettingsUi(ctx, loaded, initialScope) {
497
- return ctx.ui.custom((tui, theme, _kb, done) => {
541
+ return ctx.ui.custom((_tui, theme, _kb, done) => {
498
542
  let state = createSettingsDraftState(loaded, initialScope);
499
543
  let settingsList;
544
+ let urlEditorOpen = false;
500
545
  const container = new Container();
501
546
  container.addChild(new Text(theme.fg('accent', theme.bold('pi-web-agent · backends')), 1, 1));
502
547
  container.addChild(new Text(theme.fg('muted', 'Ctrl+S save · Ctrl+R reset scope · Esc cancel · API keys stay in env vars'), 1, 2));
503
- const editUrl = async (id) => {
504
- const isSearchUrl = id === 'backend:search:baseUrl';
505
- const label = isSearchUrl ? 'SearXNG base URL' : 'Firecrawl base URL';
506
- const currentValue = isSearchUrl ? state.backends.search.baseUrl : state.backends.fetch.baseUrl;
507
- const entered = await ctx.ui.input(label, currentValue ?? (isSearchUrl ? 'http://localhost:8080' : 'http://localhost:3002'));
508
- if (entered === undefined)
509
- return;
510
- if (!entered.trim()) {
511
- state = applySettingsValue(state, id, '');
512
- rebuildSettingsList();
513
- tui.requestRender?.();
514
- return;
515
- }
516
- const validated = validateBackendUrl(entered);
517
- if (!validated.ok) {
518
- ctx.ui.notify(validated.message, 'warning');
519
- return;
520
- }
521
- state = applySettingsValue(state, id, validated.value);
522
- rebuildSettingsList();
523
- tui.requestRender?.();
524
- };
525
548
  const rebuildSettingsList = () => {
526
549
  if (settingsList) {
527
550
  container.removeChild(settingsList);
528
551
  }
529
- settingsList = new SettingsList(buildBackendSettingsItems(state.scope, state.backends), 12, getSettingsListTheme(), (id, newValue) => {
530
- if (id === 'backend:search:baseUrl' || id === 'backend:fetch:baseUrl') {
531
- void editUrl(id);
532
- return;
533
- }
552
+ settingsList = new SettingsList(buildBackendSettingsItems(state.scope, state.backends, theme, (open) => {
553
+ urlEditorOpen = open;
554
+ }), 12, getSettingsListTheme(), (id, newValue) => {
534
555
  state = applySettingsValue(state, id, newValue);
535
556
  rebuildSettingsList();
536
557
  container.invalidate();
@@ -542,6 +563,13 @@ async function openBackendSettingsUi(ctx, loaded, initialScope) {
542
563
  render: (width) => container.render(width),
543
564
  invalidate: () => container.invalidate(),
544
565
  handleInput: (data) => {
566
+ // While the inline URL editor is open, let it (and its own Escape/Enter
567
+ // handling) consume input first — otherwise these shortcuts close the
568
+ // whole settings UI out from under an in-progress edit.
569
+ if (urlEditorOpen) {
570
+ settingsList.handleInput?.(data);
571
+ return;
572
+ }
545
573
  const shortcut = handleSettingsShortcut(JSON.stringify(data).slice(1, -1));
546
574
  if (shortcut?.action === 'cancel') {
547
575
  done({ action: 'cancel' });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@demigodmode/pi-web-agent",
3
- "version": "1.6.0",
3
+ "version": "1.6.2",
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",
@@ -13,6 +13,7 @@
13
13
  },
14
14
  "files": [
15
15
  "dist",
16
+ "scripts/patch-jiti-compat.mjs",
16
17
  "README.md",
17
18
  "CHANGELOG.md"
18
19
  ],
@@ -21,7 +22,12 @@
21
22
  "pi",
22
23
  "extension",
23
24
  "web-search",
24
- "web-fetch"
25
+ "web-fetch",
26
+ "duckduckgo",
27
+ "searxng",
28
+ "brave-search",
29
+ "you-com",
30
+ "firecrawl"
25
31
  ],
26
32
  "repository": {
27
33
  "type": "git",
@@ -36,6 +42,7 @@
36
42
  "access": "public"
37
43
  },
38
44
  "scripts": {
45
+ "postinstall": "node scripts/patch-jiti-compat.mjs",
39
46
  "build": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\" && tsc -p tsconfig.build.json",
40
47
  "build:dev": "tsc -p tsconfig.json",
41
48
  "test": "vitest run --coverage",
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env node
2
+ // Works around two incompatibilities between pi's extension loader (a patched
3
+ // jiti) and jsdom's dependency tree:
4
+ // 1. jiti can't resolve the trailing-slash bare specifier require("punycode/")
5
+ // used by tr46.
6
+ // 2. jiti wraps `module.exports = new Set(...)` (cssstyle) in a Proxy, which
7
+ // breaks native Set methods on the exported value.
8
+ //
9
+ // Runs as a postinstall step so it self-heals after every install, including
10
+ // when this package is installed as a pi extension via `pi install npm:...`.
11
+ // Safe to run multiple times and safe to no-op if the target files or
12
+ // patterns are missing (e.g. a future dependency bump changes the shape).
13
+ import { readFileSync, writeFileSync, existsSync } from "node:fs";
14
+ import { fileURLToPath } from "node:url";
15
+ import { dirname, join } from "node:path";
16
+
17
+ const __dirname = dirname(fileURLToPath(import.meta.url));
18
+ const nodeModules = join(__dirname, "..", "node_modules");
19
+
20
+ function patchTr46() {
21
+ const file = join(nodeModules, "tr46", "index.js");
22
+ if (!existsSync(file)) {
23
+ console.debug("patch-jiti-compat: tr46/index.js not found, skipping");
24
+ return;
25
+ }
26
+ const contents = readFileSync(file, "utf8");
27
+ if (!contents.includes('require("punycode/")')) {
28
+ console.debug("patch-jiti-compat: tr46/index.js does not match expected pattern, skipping");
29
+ return;
30
+ }
31
+ writeFileSync(
32
+ file,
33
+ contents.replaceAll('require("punycode/")', 'require("punycode/punycode.js")'),
34
+ );
35
+ console.log("patch-jiti-compat: patched tr46/index.js");
36
+ }
37
+
38
+ const SET_SHIM = `
39
+ // pi/jiti workaround: expose bound native Set methods as own properties so a
40
+ // Proxy wrapper around this export does not break Set brand checks.
41
+ for (const k of ["has", "add", "delete", "forEach", "keys", "values", "entries"]) {
42
+ module.exports[k] = Set.prototype[k].bind(module.exports);
43
+ }
44
+ module.exports[Symbol.iterator] = Set.prototype[Symbol.iterator].bind(module.exports);
45
+ `;
46
+
47
+ function patchCssstyleSetExports() {
48
+ const files = [
49
+ join(nodeModules, "cssstyle", "lib", "allExtraProperties.js"),
50
+ join(nodeModules, "cssstyle", "lib", "generated", "allProperties.js"),
51
+ join(nodeModules, "cssstyle", "lib", "generated", "implementedProperties.js"),
52
+ ];
53
+ for (const file of files) {
54
+ const label = `cssstyle/${file.slice(nodeModules.length + 1)}`;
55
+ if (!existsSync(file)) {
56
+ console.debug(`patch-jiti-compat: ${label} not found, skipping`);
57
+ continue;
58
+ }
59
+ const contents = readFileSync(file, "utf8");
60
+ if (contents.includes("pi/jiti workaround")) continue;
61
+ if (!contents.includes("module.exports = new Set(")) {
62
+ console.debug(`patch-jiti-compat: ${label} does not match expected pattern, skipping`);
63
+ continue;
64
+ }
65
+ writeFileSync(file, contents + SET_SHIM);
66
+ console.log(`patch-jiti-compat: patched ${label}`);
67
+ }
68
+ }
69
+
70
+ try {
71
+ patchTr46();
72
+ patchCssstyleSetExports();
73
+ } catch (err) {
74
+ // Never fail the install over a best-effort compat patch.
75
+ console.warn("patch-jiti-compat: skipped, non-fatal error:", err.message);
76
+ }