@demigodmode/pi-web-agent 0.2.1 → 0.3.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.
@@ -1,14 +1,23 @@
1
1
  import { createHttpFetcher } from '../fetch/http-fetch.js';
2
+ import { buildFetchPresentation } from '../presentation/fetch-presentation.js';
2
3
  export function createWebFetchTool({ fetchPage = createHttpFetcher() } = {}) {
3
4
  return async function webFetch({ url }) {
4
5
  if (!/^https?:\/\//.test(url)) {
5
- return {
6
+ const result = {
6
7
  status: 'unsupported',
7
8
  url,
8
9
  metadata: { method: 'http', cacheHit: false },
9
10
  error: { code: 'UNSUPPORTED_URL', message: 'Only http and https URLs are supported.' }
10
11
  };
12
+ return {
13
+ ...result,
14
+ presentation: buildFetchPresentation(result)
15
+ };
11
16
  }
12
- return fetchPage(url);
17
+ const result = await fetchPage(url);
18
+ return {
19
+ ...result,
20
+ presentation: buildFetchPresentation(result)
21
+ };
13
22
  };
14
23
  }
@@ -1,43 +1,130 @@
1
1
  import { createCacheKey, createTtlCache } from '../cache/ttl-cache.js';
2
+ import { buildSearchPresentation } from '../presentation/search-presentation.js';
2
3
  import { fetchDuckDuckGoHtml, parseDuckDuckGoResults } from '../search/duckduckgo.js';
4
+ function classifySearchFailure(error) {
5
+ const rawMessage = error instanceof Error ? error.message : 'Unknown search failure.';
6
+ const normalized = rawMessage.toLowerCase();
7
+ if (normalized.includes('blocked') ||
8
+ normalized.includes('rate limit') ||
9
+ normalized.includes('rate-limit') ||
10
+ normalized.includes('403') ||
11
+ normalized.includes('429') ||
12
+ normalized.includes('captcha') ||
13
+ normalized.includes('challenge')) {
14
+ return {
15
+ code: 'BLOCKED',
16
+ message: 'DuckDuckGo search appears to be blocked or rate limited.'
17
+ };
18
+ }
19
+ return {
20
+ code: 'FETCH_FAILED',
21
+ message: `DuckDuckGo search request failed: ${rawMessage}`
22
+ };
23
+ }
24
+ function htmlLooksBlocked(html) {
25
+ const normalized = html.toLowerCase();
26
+ return (normalized.includes('captcha') ||
27
+ normalized.includes('challenge') ||
28
+ normalized.includes('verify you are human') ||
29
+ normalized.includes('are you a robot') ||
30
+ normalized.includes('unusual traffic'));
31
+ }
3
32
  export function createWebSearchTool({ searchHtml = fetchDuckDuckGoHtml, cache = createTtlCache({ ttlMs: 30_000 }) } = {}) {
4
33
  return async function webSearch({ query }) {
5
34
  const normalizedQuery = query.trim();
6
35
  if (!normalizedQuery) {
7
- return {
36
+ const result = {
8
37
  status: 'error',
9
38
  results: [],
10
39
  metadata: { backend: 'duckduckgo', cacheHit: false },
11
40
  error: { code: 'INVALID_QUERY', message: 'Query must not be empty.' }
12
41
  };
42
+ return {
43
+ ...result,
44
+ presentation: buildSearchPresentation(result)
45
+ };
13
46
  }
14
47
  const cacheKey = createCacheKey(['web_search', normalizedQuery]);
15
48
  const cached = cache.get(cacheKey);
16
49
  if (cached) {
17
- return {
50
+ const result = {
18
51
  ...cached,
19
52
  metadata: { ...cached.metadata, cacheHit: true }
20
53
  };
54
+ return {
55
+ ...result,
56
+ presentation: buildSearchPresentation(result)
57
+ };
21
58
  }
22
59
  try {
23
60
  const html = await searchHtml(normalizedQuery);
61
+ const parsed = parseDuckDuckGoResults(html);
62
+ if (parsed.results.length > 0) {
63
+ const result = {
64
+ status: 'ok',
65
+ results: parsed.results,
66
+ metadata: { backend: 'duckduckgo', cacheHit: false }
67
+ };
68
+ cache.set(cacheKey, result);
69
+ return {
70
+ ...result,
71
+ presentation: buildSearchPresentation(result)
72
+ };
73
+ }
74
+ if (parsed.noResults) {
75
+ const result = {
76
+ status: 'error',
77
+ results: [],
78
+ metadata: { backend: 'duckduckgo', cacheHit: false },
79
+ error: {
80
+ code: 'NO_RESULTS',
81
+ message: 'DuckDuckGo returned no usable results for this query.'
82
+ }
83
+ };
84
+ return {
85
+ ...result,
86
+ presentation: buildSearchPresentation(result)
87
+ };
88
+ }
89
+ if (htmlLooksBlocked(html)) {
90
+ const result = {
91
+ status: 'error',
92
+ results: [],
93
+ metadata: { backend: 'duckduckgo', cacheHit: false },
94
+ error: {
95
+ code: 'BLOCKED',
96
+ message: 'DuckDuckGo search appears to be blocked or rate limited.'
97
+ }
98
+ };
99
+ return {
100
+ ...result,
101
+ presentation: buildSearchPresentation(result)
102
+ };
103
+ }
24
104
  const result = {
25
- status: 'ok',
26
- results: parseDuckDuckGoResults(html),
27
- metadata: { backend: 'duckduckgo', cacheHit: false }
105
+ status: 'error',
106
+ results: [],
107
+ metadata: { backend: 'duckduckgo', cacheHit: false },
108
+ error: {
109
+ code: 'PARSE_FAILED',
110
+ message: 'DuckDuckGo returned a page, but it did not match the expected results format.'
111
+ }
112
+ };
113
+ return {
114
+ ...result,
115
+ presentation: buildSearchPresentation(result)
28
116
  };
29
- cache.set(cacheKey, result);
30
- return result;
31
117
  }
32
118
  catch (error) {
33
- return {
119
+ const result = {
34
120
  status: 'error',
35
121
  results: [],
36
122
  metadata: { backend: 'duckduckgo', cacheHit: false },
37
- error: {
38
- code: 'SEARCH_FAILED',
39
- message: error instanceof Error ? error.message : 'Unknown search failure.'
40
- }
123
+ error: classifySearchFailure(error)
124
+ };
125
+ return {
126
+ ...result,
127
+ presentation: buildSearchPresentation(result)
41
128
  };
42
129
  }
43
130
  };
package/dist/types.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { PresentationEnvelope } from './presentation/types.js';
1
2
  export declare const TOOL_STATUSES: readonly ["ok", "needs_headless", "blocked", "unsupported", "error"];
2
3
  export type ToolStatus = (typeof TOOL_STATUSES)[number];
3
4
  export type SearchResult = {
@@ -30,6 +31,7 @@ export type WebSearchResponse = {
30
31
  status: 'ok' | 'error';
31
32
  results: SearchResult[];
32
33
  metadata: SearchMetadata;
34
+ presentation?: PresentationEnvelope;
33
35
  error?: ToolError;
34
36
  };
35
37
  export type WebFetchResponse = {
@@ -37,6 +39,7 @@ export type WebFetchResponse = {
37
39
  url: string;
38
40
  content?: ExtractedContent;
39
41
  metadata: FetchMetadata;
42
+ presentation?: PresentationEnvelope;
40
43
  error?: ToolError;
41
44
  };
42
45
  export type WebFetchHeadlessResponse = {
@@ -44,5 +47,17 @@ export type WebFetchHeadlessResponse = {
44
47
  url: string;
45
48
  content?: ExtractedContent;
46
49
  metadata: FetchMetadata;
50
+ presentation?: PresentationEnvelope;
51
+ error?: ToolError;
52
+ };
53
+ export type WebExploreResponse = {
54
+ status: 'ok' | 'error';
55
+ findings: string[];
56
+ sources: Array<{
57
+ title: string;
58
+ url: string;
59
+ }>;
60
+ caveat?: string;
61
+ presentation?: PresentationEnvelope;
47
62
  error?: ToolError;
48
63
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@demigodmode/pi-web-agent",
3
- "version": "0.2.1",
3
+ "version": "0.3.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",
@@ -40,6 +40,9 @@
40
40
  "test": "vitest run --coverage",
41
41
  "test:watch": "vitest",
42
42
  "lint": "tsc -p tsconfig.json --noEmit",
43
+ "docs:dev": "vitepress dev docs",
44
+ "docs:build": "vitepress build docs",
45
+ "docs:preview": "vitepress preview docs",
43
46
  "eval:live": "npm run build:dev && node dist/scripts/live-web-eval.js",
44
47
  "release:dry-run": "node scripts/release.mjs --dry-run",
45
48
  "release": "node scripts/release.mjs"
@@ -62,6 +65,7 @@
62
65
  "@types/node": "^24.0.0",
63
66
  "@vitest/coverage-v8": "^3.2.4",
64
67
  "typescript": "^5.8.0",
68
+ "vitepress": "^1.6.4",
65
69
  "vitest": "^3.2.0"
66
70
  },
67
71
  "peerDependencies": {