@demigodmode/pi-web-agent 1.5.1 → 1.6.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/CHANGELOG.md CHANGED
@@ -18,6 +18,37 @@ The format is intentionally simple and release-oriented.
18
18
  ### Breaking
19
19
  - None.
20
20
 
21
+ ## [1.6.1] - 2026-07-05
22
+ ### Added
23
+ - None.
24
+
25
+ ### Changed
26
+ - None.
27
+
28
+ ### Fixed
29
+ - 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.
30
+ - 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.
31
+ - 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.
32
+ - 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.
33
+
34
+ ### Breaking
35
+ - None.
36
+
37
+ ## [1.6.0] - 2026-07-05
38
+ ### Added
39
+ - 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.
40
+ - `/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`.
41
+ - `/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.
42
+
43
+ ### Changed
44
+ - None.
45
+
46
+ ### Fixed
47
+ - None.
48
+
49
+ ### Breaking
50
+ - None.
51
+
21
52
  ## [1.5.1] - 2026-06-22
22
53
  ### Added
23
54
  - None.
package/README.md CHANGED
@@ -27,7 +27,7 @@ Headless rendering first tries a detectable Chromium-family browser: Chrome, Chr
27
27
  Later on, update installed packages with:
28
28
 
29
29
  ```bash
30
- pi update
30
+ pi update --extensions
31
31
  ```
32
32
 
33
33
  ## Docs
@@ -109,7 +109,9 @@ Example:
109
109
  }
110
110
  ```
111
111
 
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 API key, Brave can be selected as a hosted search backend while `web_explore` still handles page reading, ranking, and caveats itself.
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
+
114
+ Search backends: DuckDuckGo (default), SearXNG (self-hosted), Brave Search (hosted), You.com (hosted).
113
115
 
114
116
  Backend settings can be changed from:
115
117
 
@@ -117,9 +119,9 @@ Backend settings can be changed from:
117
119
  /web-agent settings
118
120
  ```
119
121
 
120
- Choose **Backends** to edit search/fetch providers, fallback behavior, and SearXNG or Firecrawl base URLs interactively. Brave Search uses `PI_WEB_AGENT_BRAVE_API_KEY`. Firecrawl API keys should also stay in environment variables rather than being written into config files.
122
+ Choose **Backends** to edit search/fetch providers, fallback behavior, and SearXNG or Firecrawl base URLs interactively. Brave Search uses `PI_WEB_AGENT_BRAVE_API_KEY` and You.com uses `YDC_API_KEY`. Firecrawl API keys should also stay in environment variables rather than being written into config files.
121
123
 
122
- For the full backend config shape, including SearXNG, Brave, Firecrawl, and fallback behavior, see:
124
+ For the full backend config shape, including SearXNG, Brave, You.com, Firecrawl, and fallback behavior, see:
123
125
 
124
126
  - https://demigodmode.github.io/pi-web-agent/self-hosted-backends
125
127
 
@@ -8,7 +8,7 @@ export type FirecrawlOptions = {
8
8
  onlyMainContent?: boolean;
9
9
  };
10
10
  export type SearchBackendConfig = {
11
- provider: 'duckduckgo' | 'searxng' | 'brave';
11
+ provider: 'duckduckgo' | 'searxng' | 'brave' | 'youcom';
12
12
  baseUrl?: string;
13
13
  fallback?: 'duckduckgo';
14
14
  options?: SearxngOptions;
@@ -40,7 +40,8 @@ export function extractBackendConfigOverride(file) {
40
40
  const override = {};
41
41
  if (backends?.search?.provider === 'duckduckgo' ||
42
42
  backends?.search?.provider === 'searxng' ||
43
- backends?.search?.provider === 'brave') {
43
+ backends?.search?.provider === 'brave' ||
44
+ backends?.search?.provider === 'youcom') {
44
45
  override.search = { provider: backends.search.provider };
45
46
  if (backends.search.provider === 'searxng' && typeof backends.search.baseUrl === 'string') {
46
47
  override.search.baseUrl = backends.search.baseUrl;
@@ -84,8 +85,8 @@ export function validateBackendConfig(config) {
84
85
  if (config.fetch.provider === 'firecrawl' && !config.fetch.baseUrl) {
85
86
  issues.push('fetch provider firecrawl requires backends.fetch.baseUrl');
86
87
  }
87
- if (config.search.fallback === 'duckduckgo' && config.search.provider !== 'searxng' && config.search.provider !== 'brave') {
88
- issues.push('search fallback duckduckgo is only supported when search provider is searxng or brave');
88
+ if (config.search.fallback === 'duckduckgo' && config.search.provider !== 'searxng' && config.search.provider !== 'brave' && config.search.provider !== 'youcom') {
89
+ issues.push('search fallback duckduckgo is only supported when search provider is searxng, brave, or youcom');
89
90
  }
90
91
  if (config.fetch.fallback === 'http' && config.fetch.provider !== 'firecrawl') {
91
92
  issues.push('fetch fallback http is only supported when fetch provider is firecrawl');
@@ -12,6 +12,9 @@ function braveDoctorUrl() {
12
12
  url.searchParams.set('count', '1');
13
13
  return url.toString();
14
14
  }
15
+ function youcomDoctorBody() {
16
+ return JSON.stringify({ query: 'pi-web-agent-doctor', max_results: 1 });
17
+ }
15
18
  function searxngDoctorUrl(baseUrl, options = {}) {
16
19
  const url = new URL('/search', baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`);
17
20
  url.searchParams.set('q', 'pi-web-agent-doctor');
@@ -69,6 +72,42 @@ export async function checkBackendHealth(config, { fetchImpl = fetch, timeoutMs
69
72
  }
70
73
  }
71
74
  }
75
+ else if (config.search.provider === 'youcom') {
76
+ const apiKey = process.env.YDC_API_KEY;
77
+ if (!apiKey?.trim()) {
78
+ lines.push('search backend: youcom warning (missing YDC_API_KEY)');
79
+ }
80
+ else {
81
+ const timeout = withTimeout(timeoutMs);
82
+ try {
83
+ const response = await fetchImpl('https://api.you.com/v1/agents/search', {
84
+ method: 'POST',
85
+ headers: {
86
+ Accept: 'application/json',
87
+ 'Content-Type': 'application/json',
88
+ 'X-API-Key': apiKey
89
+ },
90
+ body: youcomDoctorBody(),
91
+ signal: timeout.signal
92
+ });
93
+ if (!response.ok) {
94
+ lines.push(`search backend: youcom warning (HTTP ${response.status})`);
95
+ }
96
+ else {
97
+ const json = (await response.json());
98
+ lines.push(Array.isArray(json.results)
99
+ ? 'search backend: youcom ok'
100
+ : 'search backend: youcom warning (unexpected response)');
101
+ }
102
+ }
103
+ catch (error) {
104
+ lines.push(`search backend: youcom warning (${message(error)})`);
105
+ }
106
+ finally {
107
+ timeout.done();
108
+ }
109
+ }
110
+ }
72
111
  else if (!config.search.baseUrl) {
73
112
  lines.push('search backend: searxng warning (missing baseUrl)');
74
113
  }
@@ -1,5 +1,6 @@
1
1
  import { createFirecrawlFetcher } from '../fetch/firecrawl-fetch.js';
2
2
  import { createBraveSearchTool } from '../search/brave.js';
3
+ import { createYouComSearchTool } from '../search/youcom.js';
3
4
  import { createSearxngSearchTool } from '../search/searxng.js';
4
5
  import { createWebFetchHeadlessTool } from '../tools/web-fetch-headless.js';
5
6
  import { createWebFetchTool } from '../tools/web-fetch.js';
@@ -21,6 +22,7 @@ export type BackendFactoryDeps = {
21
22
  createDuckDuckGoSearch?: typeof createWebSearchTool;
22
23
  createSearxngSearch?: typeof createSearxngSearchTool;
23
24
  createBraveSearch?: typeof createBraveSearchTool;
25
+ createYouComSearch?: typeof createYouComSearchTool;
24
26
  createHttpFetch?: typeof createWebFetchTool;
25
27
  createFirecrawlFetch?: typeof createFirecrawlFetcher;
26
28
  createHeadlessFetch?: typeof createWebFetchHeadlessTool;
@@ -1,5 +1,6 @@
1
1
  import { createFirecrawlFetcher } from '../fetch/firecrawl-fetch.js';
2
2
  import { createBraveSearchTool } from '../search/brave.js';
3
+ import { createYouComSearchTool } from '../search/youcom.js';
3
4
  import { createSearxngSearchTool } from '../search/searxng.js';
4
5
  import { buildFetchPresentation } from '../presentation/fetch-presentation.js';
5
6
  import { buildSearchPresentation } from '../presentation/search-presentation.js';
@@ -73,6 +74,7 @@ export function createBackendSet(config = DEFAULT_BACKEND_CONFIG, deps = {}) {
73
74
  const createDuckDuckGoSearch = deps.createDuckDuckGoSearch ?? createWebSearchTool;
74
75
  const createSearxngSearch = deps.createSearxngSearch ?? createSearxngSearchTool;
75
76
  const createBraveSearch = deps.createBraveSearch ?? createBraveSearchTool;
77
+ const createYouComSearch = deps.createYouComSearch ?? createYouComSearchTool;
76
78
  const createHttpFetch = deps.createHttpFetch ?? createWebFetchTool;
77
79
  const createFirecrawlFetch = deps.createFirecrawlFetch ?? createFirecrawlFetcher;
78
80
  const createHeadlessFetch = deps.createHeadlessFetch ?? createWebFetchHeadlessTool;
@@ -82,13 +84,18 @@ export function createBackendSet(config = DEFAULT_BACKEND_CONFIG, deps = {}) {
82
84
  : invalidSearxngSearch()
83
85
  : config.search.provider === 'brave'
84
86
  ? createBraveSearch({ apiKey: process.env.PI_WEB_AGENT_BRAVE_API_KEY })
85
- : createDuckDuckGoSearch();
87
+ : config.search.provider === 'youcom'
88
+ ? createYouComSearch({ apiKey: process.env.YDC_API_KEY })
89
+ : createDuckDuckGoSearch();
86
90
  if (config.search.provider === 'searxng' && config.search.fallback === 'duckduckgo') {
87
91
  search = withSearchFallback(search, createDuckDuckGoSearch(), 'searxng');
88
92
  }
89
93
  if (config.search.provider === 'brave' && config.search.fallback === 'duckduckgo') {
90
94
  search = withSearchFallback(search, createDuckDuckGoSearch(), 'brave');
91
95
  }
96
+ if (config.search.provider === 'youcom' && config.search.fallback === 'duckduckgo') {
97
+ search = withSearchFallback(search, createDuckDuckGoSearch(), 'youcom');
98
+ }
92
99
  const httpFetch = createHttpFetch();
93
100
  let fetchPage = config.fetch.provider === 'firecrawl'
94
101
  ? config.fetch.baseUrl
@@ -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',
@@ -124,19 +166,19 @@ function buildBackendSettingsItems(scope, backends) {
124
166
  id: 'backend:search:provider',
125
167
  label: 'Search backend',
126
168
  currentValue: backends.search.provider,
127
- values: ['duckduckgo', 'searxng', 'brave']
169
+ values: ['duckduckgo', 'searxng', 'brave', 'youcom']
128
170
  },
129
171
  {
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',
137
179
  label: 'Search fallback',
138
- currentValue: backends.search.provider === 'searxng' || backends.search.provider === 'brave' ? backends.search.fallback ?? 'off' : 'off',
139
- values: backends.search.provider === 'searxng' || backends.search.provider === 'brave' ? ['off', 'duckduckgo'] : ['off']
180
+ currentValue: backends.search.provider === 'searxng' || backends.search.provider === 'brave' || backends.search.provider === 'youcom' ? backends.search.fallback ?? 'off' : 'off',
181
+ values: backends.search.provider === 'searxng' || backends.search.provider === 'brave' || backends.search.provider === 'youcom' ? ['off', 'duckduckgo'] : ['off']
140
182
  },
141
183
  {
142
184
  id: 'backend:secret:brave',
@@ -144,6 +186,12 @@ function buildBackendSettingsItems(scope, backends) {
144
186
  currentValue: 'env var',
145
187
  values: ['env var']
146
188
  },
189
+ {
190
+ id: 'backend:secret:youcom',
191
+ label: 'You.com API key',
192
+ currentValue: 'env var',
193
+ values: ['env var']
194
+ },
147
195
  {
148
196
  id: 'backend:fetch:provider',
149
197
  label: 'Fetch backend',
@@ -154,7 +202,7 @@ function buildBackendSettingsItems(scope, backends) {
154
202
  id: 'backend:fetch:baseUrl',
155
203
  label: 'Firecrawl URL',
156
204
  currentValue: backends.fetch.baseUrl ?? 'not set',
157
- values: ['edit']
205
+ submenu: createBackendUrlEditor(theme, 'Firecrawl base URL', 'http://localhost:3002', onUrlEditorOpenChange)
158
206
  },
159
207
  {
160
208
  id: 'backend:fetch:fallback',
@@ -255,7 +303,7 @@ export function applySettingsValue(state, id, newValue) {
255
303
  }
256
304
  currentDraft.tools = nextTools;
257
305
  }
258
- if (id === 'backend:search:provider' && (newValue === 'duckduckgo' || newValue === 'searxng' || newValue === 'brave')) {
306
+ if (id === 'backend:search:provider' && (newValue === 'duckduckgo' || newValue === 'searxng' || newValue === 'brave' || newValue === 'youcom')) {
259
307
  currentBackends.search.provider = newValue;
260
308
  if (newValue !== 'searxng') {
261
309
  delete currentBackends.search.baseUrl;
@@ -266,7 +314,7 @@ export function applySettingsValue(state, id, newValue) {
266
314
  }
267
315
  }
268
316
  if (id === 'backend:search:fallback') {
269
- if (newValue === 'duckduckgo' && (currentBackends.search.provider === 'searxng' || currentBackends.search.provider === 'brave')) {
317
+ if (newValue === 'duckduckgo' && (currentBackends.search.provider === 'searxng' || currentBackends.search.provider === 'brave' || currentBackends.search.provider === 'youcom')) {
270
318
  currentBackends.search.fallback = 'duckduckgo';
271
319
  }
272
320
  else {
@@ -274,7 +322,8 @@ export function applySettingsValue(state, id, newValue) {
274
322
  }
275
323
  }
276
324
  if (id === 'backend:search:baseUrl') {
277
- if (currentBackends.search.provider === 'searxng' && newValue.trim()) {
325
+ if (newValue.trim()) {
326
+ currentBackends.search.provider = 'searxng';
278
327
  currentBackends.search.baseUrl = newValue.trim();
279
328
  }
280
329
  else {
@@ -297,6 +346,7 @@ export function applySettingsValue(state, id, newValue) {
297
346
  }
298
347
  if (id === 'backend:fetch:baseUrl') {
299
348
  if (newValue.trim()) {
349
+ currentBackends.fetch.provider = 'firecrawl';
300
350
  currentBackends.fetch.baseUrl = newValue.trim();
301
351
  }
302
352
  else {
@@ -488,43 +538,20 @@ async function openPresentationSettingsUi(ctx, loaded, initialScope) {
488
538
  });
489
539
  }
490
540
  async function openBackendSettingsUi(ctx, loaded, initialScope) {
491
- return ctx.ui.custom((tui, theme, _kb, done) => {
541
+ return ctx.ui.custom((_tui, theme, _kb, done) => {
492
542
  let state = createSettingsDraftState(loaded, initialScope);
493
543
  let settingsList;
544
+ let urlEditorOpen = false;
494
545
  const container = new Container();
495
546
  container.addChild(new Text(theme.fg('accent', theme.bold('pi-web-agent · backends')), 1, 1));
496
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));
497
- const editUrl = async (id) => {
498
- const isSearchUrl = id === 'backend:search:baseUrl';
499
- const label = isSearchUrl ? 'SearXNG base URL' : 'Firecrawl base URL';
500
- const currentValue = isSearchUrl ? state.backends.search.baseUrl : state.backends.fetch.baseUrl;
501
- const entered = await ctx.ui.input(label, currentValue ?? (isSearchUrl ? 'http://localhost:8080' : 'http://localhost:3002'));
502
- if (entered === undefined)
503
- return;
504
- if (!entered.trim()) {
505
- state = applySettingsValue(state, id, '');
506
- rebuildSettingsList();
507
- tui.requestRender?.();
508
- return;
509
- }
510
- const validated = validateBackendUrl(entered);
511
- if (!validated.ok) {
512
- ctx.ui.notify(validated.message, 'warning');
513
- return;
514
- }
515
- state = applySettingsValue(state, id, validated.value);
516
- rebuildSettingsList();
517
- tui.requestRender?.();
518
- };
519
548
  const rebuildSettingsList = () => {
520
549
  if (settingsList) {
521
550
  container.removeChild(settingsList);
522
551
  }
523
- settingsList = new SettingsList(buildBackendSettingsItems(state.scope, state.backends), 12, getSettingsListTheme(), (id, newValue) => {
524
- if (id === 'backend:search:baseUrl' || id === 'backend:fetch:baseUrl') {
525
- void editUrl(id);
526
- return;
527
- }
552
+ settingsList = new SettingsList(buildBackendSettingsItems(state.scope, state.backends, theme, (open) => {
553
+ urlEditorOpen = open;
554
+ }), 12, getSettingsListTheme(), (id, newValue) => {
528
555
  state = applySettingsValue(state, id, newValue);
529
556
  rebuildSettingsList();
530
557
  container.invalidate();
@@ -536,6 +563,13 @@ async function openBackendSettingsUi(ctx, loaded, initialScope) {
536
563
  render: (width) => container.render(width),
537
564
  invalidate: () => container.invalidate(),
538
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
+ }
539
573
  const shortcut = handleSettingsShortcut(JSON.stringify(data).slice(1, -1));
540
574
  if (shortcut?.action === 'cancel') {
541
575
  done({ action: 'cancel' });
@@ -0,0 +1,7 @@
1
+ import type { WebSearchResponse } from '../types.js';
2
+ export declare function createYouComSearchTool({ apiKey, fetchImpl }: {
3
+ apiKey?: string;
4
+ fetchImpl?: typeof fetch;
5
+ }): ({ query }: {
6
+ query: string;
7
+ }) => Promise<WebSearchResponse>;
@@ -0,0 +1,81 @@
1
+ import { buildSearchPresentation } from '../presentation/search-presentation.js';
2
+ const YOUCOM_SEARCH_URL = 'https://api.you.com/v1/agents/search';
3
+ function resultWithPresentation(result) {
4
+ return { ...result, presentation: buildSearchPresentation(result) };
5
+ }
6
+ function normalizeResults(response) {
7
+ return (response.results ?? []).flatMap((item) => {
8
+ if (typeof item.title !== 'string' || typeof item.url !== 'string') {
9
+ return [];
10
+ }
11
+ return [
12
+ {
13
+ title: item.title,
14
+ url: item.url,
15
+ snippet: typeof item.snippet === 'string' ? item.snippet : ''
16
+ }
17
+ ];
18
+ });
19
+ }
20
+ export function createYouComSearchTool({ apiKey, fetchImpl = fetch }) {
21
+ return async function youComSearch({ query }) {
22
+ const normalizedQuery = query.trim();
23
+ if (!normalizedQuery) {
24
+ return resultWithPresentation({
25
+ status: 'error',
26
+ results: [],
27
+ metadata: { backend: 'youcom', cacheHit: false },
28
+ error: { code: 'INVALID_QUERY', message: 'Query must not be empty.' }
29
+ });
30
+ }
31
+ if (!apiKey?.trim()) {
32
+ return resultWithPresentation({
33
+ status: 'error',
34
+ results: [],
35
+ metadata: { backend: 'youcom', cacheHit: false },
36
+ error: {
37
+ code: 'BACKEND_CONFIG_INVALID',
38
+ message: 'You.com search requires YDC_API_KEY.'
39
+ }
40
+ });
41
+ }
42
+ try {
43
+ const response = await fetchImpl(YOUCOM_SEARCH_URL, {
44
+ method: 'POST',
45
+ headers: {
46
+ Accept: 'application/json',
47
+ 'Content-Type': 'application/json',
48
+ 'X-API-Key': apiKey
49
+ },
50
+ body: JSON.stringify({ query: normalizedQuery, max_results: 10 })
51
+ });
52
+ if (!response.ok) {
53
+ throw new Error(`HTTP ${response.status}`);
54
+ }
55
+ const parsed = (await response.json());
56
+ const results = normalizeResults(parsed);
57
+ if (results.length === 0) {
58
+ return resultWithPresentation({
59
+ status: 'error',
60
+ results: [],
61
+ metadata: { backend: 'youcom', cacheHit: false },
62
+ error: { code: 'NO_RESULTS', message: 'You.com returned no usable results for this query.' }
63
+ });
64
+ }
65
+ return resultWithPresentation({
66
+ status: 'ok',
67
+ results,
68
+ metadata: { backend: 'youcom', cacheHit: false }
69
+ });
70
+ }
71
+ catch (error) {
72
+ const rawMessage = error instanceof Error ? error.message : String(error);
73
+ return resultWithPresentation({
74
+ status: 'error',
75
+ results: [],
76
+ metadata: { backend: 'youcom', cacheHit: false },
77
+ error: { code: 'FETCH_FAILED', message: `You.com search request failed: ${rawMessage}` }
78
+ });
79
+ }
80
+ };
81
+ }
package/dist/types.d.ts CHANGED
@@ -11,9 +11,9 @@ export type ToolError = {
11
11
  message: string;
12
12
  };
13
13
  export type SearchMetadata = {
14
- backend: 'duckduckgo' | 'searxng' | 'brave';
14
+ backend: 'duckduckgo' | 'searxng' | 'brave' | 'youcom';
15
15
  cacheHit: boolean;
16
- fallbackFrom?: 'searxng' | 'brave';
16
+ fallbackFrom?: 'searxng' | 'brave' | 'youcom';
17
17
  fallbackReason?: string;
18
18
  };
19
19
  export type FetchMetadata = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@demigodmode/pi-web-agent",
3
- "version": "1.5.1",
3
+ "version": "1.6.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",
@@ -21,7 +21,12 @@
21
21
  "pi",
22
22
  "extension",
23
23
  "web-search",
24
- "web-fetch"
24
+ "web-fetch",
25
+ "duckduckgo",
26
+ "searxng",
27
+ "brave-search",
28
+ "you-com",
29
+ "firecrawl"
25
30
  ],
26
31
  "repository": {
27
32
  "type": "git",