@pi-unipi/web-api 2.4.0 → 2.5.0
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/package.json +2 -2
- package/src/engine/extract.ts +0 -26
- package/src/tools.ts +0 -13
- package/src/tui/progress.ts +0 -168
- package/src/tui/result.ts +0 -173
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-unipi/web-api",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.0",
|
|
4
4
|
"description": "Web search, read, and summarize tools with provider-based backend selection for Pi coding agent",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"README.md"
|
|
32
32
|
],
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@pi-unipi/core": "2.
|
|
34
|
+
"@pi-unipi/core": "2.5.0",
|
|
35
35
|
"defuddle": "^0.18.1",
|
|
36
36
|
"linkedom": "^0.18.12",
|
|
37
37
|
"lodash": "^4.17.21",
|
package/src/engine/extract.ts
CHANGED
|
@@ -35,7 +35,6 @@ import { truncateContent, formatContent } from "./format.js";
|
|
|
35
35
|
const MAX_REDIRECTS = 5;
|
|
36
36
|
|
|
37
37
|
/** Maximum alternate link fallbacks to try */
|
|
38
|
-
const MAX_ALTERNATE_LINKS = 3;
|
|
39
38
|
|
|
40
39
|
/**
|
|
41
40
|
* Validate a URL for fetching.
|
|
@@ -193,31 +192,6 @@ function findMetaRefresh(document: Document): string | null {
|
|
|
193
192
|
return match[1];
|
|
194
193
|
}
|
|
195
194
|
|
|
196
|
-
/**
|
|
197
|
-
* Check for alternate JSON content links.
|
|
198
|
-
*
|
|
199
|
-
* @param document - DOM document
|
|
200
|
-
* @returns Array of alternate URLs
|
|
201
|
-
*/
|
|
202
|
-
function findAlternateLinks(document: Document): string[] {
|
|
203
|
-
const alternates: string[] = [];
|
|
204
|
-
|
|
205
|
-
// Look for JSON feeds, oEmbed, etc.
|
|
206
|
-
const links = document.querySelectorAll(
|
|
207
|
-
'link[rel="alternate"][type="application/json"], ' +
|
|
208
|
-
'link[rel="alternate"][type="application/ld+json"]'
|
|
209
|
-
);
|
|
210
|
-
|
|
211
|
-
for (const link of Array.from(links)) {
|
|
212
|
-
const href = link.getAttribute("href");
|
|
213
|
-
if (href) {
|
|
214
|
-
alternates.push(href);
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
return alternates.slice(0, MAX_ALTERNATE_LINKS);
|
|
219
|
-
}
|
|
220
|
-
|
|
221
195
|
/**
|
|
222
196
|
* Detect content type from response.
|
|
223
197
|
*/
|
package/src/tools.ts
CHANGED
|
@@ -62,19 +62,6 @@ function getAvailableProviders(capability: WebCapability): WebProvider[] {
|
|
|
62
62
|
});
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
/**
|
|
66
|
-
* Select provider for a capability.
|
|
67
|
-
* If sourceRank is specified, use that rank.
|
|
68
|
-
* Otherwise, use the lowest-ranked available provider.
|
|
69
|
-
*/
|
|
70
|
-
function selectProvider(
|
|
71
|
-
capability: WebCapability,
|
|
72
|
-
sourceRank?: number
|
|
73
|
-
): WebProvider {
|
|
74
|
-
const candidates = selectProviderChain(capability, sourceRank);
|
|
75
|
-
return candidates[0];
|
|
76
|
-
}
|
|
77
|
-
|
|
78
65
|
/**
|
|
79
66
|
* Build the ordered list of providers to try for a capability.
|
|
80
67
|
*
|
package/src/tui/progress.ts
DELETED
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @unipi/web-api — TUI Progress Renderer
|
|
3
|
-
*
|
|
4
|
-
* Renders batch fetch progress for TUI display.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import type { FetchProgress, FetchProgressStatus } from "../engine/types.js";
|
|
8
|
-
|
|
9
|
-
/** Spinner frames for animation */
|
|
10
|
-
const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
11
|
-
|
|
12
|
-
/** Status glyphs */
|
|
13
|
-
const STATUS_GLYPHS: Record<FetchProgressStatus, string> = {
|
|
14
|
-
queued: "○",
|
|
15
|
-
connecting: SPINNER_FRAMES[0],
|
|
16
|
-
waiting: SPINNER_FRAMES[0],
|
|
17
|
-
loading: SPINNER_FRAMES[0],
|
|
18
|
-
processing: SPINNER_FRAMES[0],
|
|
19
|
-
done: "✓",
|
|
20
|
-
error: "✗",
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Get a spinner frame for the given index.
|
|
25
|
-
* Cycles through spinner frames for animation.
|
|
26
|
-
*
|
|
27
|
-
* @param index - Animation frame index
|
|
28
|
-
* @returns Spinner character
|
|
29
|
-
*/
|
|
30
|
-
export function getSpinnerFrame(index: number): string {
|
|
31
|
-
return SPINNER_FRAMES[index % SPINNER_FRAMES.length];
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Render a progress bar.
|
|
36
|
-
*
|
|
37
|
-
* @param percent - Progress percentage (0-100)
|
|
38
|
-
* @param width - Bar width in characters
|
|
39
|
-
* @returns Progress bar string
|
|
40
|
-
*/
|
|
41
|
-
export function renderProgressBar(percent: number, width: number = 10): string {
|
|
42
|
-
const filled = Math.round((percent / 100) * width);
|
|
43
|
-
const empty = width - filled;
|
|
44
|
-
return "█".repeat(filled) + "░".repeat(empty);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Truncate a URL for display.
|
|
49
|
-
*
|
|
50
|
-
* @param url - URL to truncate
|
|
51
|
-
* @param maxLength - Maximum length
|
|
52
|
-
* @returns Truncated URL
|
|
53
|
-
*/
|
|
54
|
-
function truncateUrl(url: string, maxLength: number): string {
|
|
55
|
-
if (url.length <= maxLength) {
|
|
56
|
-
return url;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// Try to keep the domain
|
|
60
|
-
try {
|
|
61
|
-
const parsed = new URL(url);
|
|
62
|
-
const domain = parsed.host;
|
|
63
|
-
const path = parsed.pathname + parsed.search;
|
|
64
|
-
|
|
65
|
-
if (domain.length + 3 >= maxLength) {
|
|
66
|
-
return url.slice(0, maxLength - 1) + "…";
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const remaining = maxLength - domain.length - 3;
|
|
70
|
-
if (path.length <= remaining) {
|
|
71
|
-
return domain + path;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
return domain + path.slice(0, remaining - 1) + "…";
|
|
75
|
-
} catch {
|
|
76
|
-
return url.slice(0, maxLength - 1) + "…";
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Render a single progress item line.
|
|
82
|
-
*
|
|
83
|
-
* @param progress - Progress object
|
|
84
|
-
* @param width - Available width
|
|
85
|
-
* @param spinnerIndex - Animation frame index
|
|
86
|
-
* @returns Formatted line
|
|
87
|
-
*/
|
|
88
|
-
export function renderProgressLine(
|
|
89
|
-
progress: FetchProgress,
|
|
90
|
-
width: number = 80,
|
|
91
|
-
spinnerIndex: number = 0
|
|
92
|
-
): string {
|
|
93
|
-
// Status glyph
|
|
94
|
-
let glyph = STATUS_GLYPHS[progress.status];
|
|
95
|
-
if (["connecting", "waiting", "loading", "processing"].includes(progress.status)) {
|
|
96
|
-
glyph = getSpinnerFrame(spinnerIndex);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// Truncate URL
|
|
100
|
-
const urlMax = Math.min(40, width - 30);
|
|
101
|
-
const url = truncateUrl(progress.url, urlMax);
|
|
102
|
-
|
|
103
|
-
// Progress bar
|
|
104
|
-
const bar = renderProgressBar(progress.percent, 8);
|
|
105
|
-
|
|
106
|
-
// Status text
|
|
107
|
-
const statusText = progress.phase || progress.status;
|
|
108
|
-
|
|
109
|
-
// Format line
|
|
110
|
-
return `${glyph} ${url.padEnd(urlMax)} ${statusText.padEnd(12)} [${bar}]`;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Render batch progress header.
|
|
115
|
-
*
|
|
116
|
-
* @param progress - All progress items
|
|
117
|
-
* @param concurrency - Current concurrency
|
|
118
|
-
* @returns Header line
|
|
119
|
-
*/
|
|
120
|
-
export function renderBatchProgressHeader(
|
|
121
|
-
progress: FetchProgress[],
|
|
122
|
-
concurrency: number
|
|
123
|
-
): string {
|
|
124
|
-
const total = progress.length;
|
|
125
|
-
const done = progress.filter((p) => p.status === "done").length;
|
|
126
|
-
const error = progress.filter((p) => p.status === "error").length;
|
|
127
|
-
const active = progress.filter(
|
|
128
|
-
(p) => !["queued", "done", "error"].includes(p.status)
|
|
129
|
-
).length;
|
|
130
|
-
|
|
131
|
-
return `batch_web_content_read ${done}/${total} done · ok ${done - error} · err ${error} · concurrency ${concurrency}`;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Render full batch progress display.
|
|
136
|
-
*
|
|
137
|
-
* @param progress - All progress items
|
|
138
|
-
* @param concurrency - Current concurrency
|
|
139
|
-
* @param width - Available width
|
|
140
|
-
* @param spinnerIndex - Animation frame index
|
|
141
|
-
* @returns Formatted string
|
|
142
|
-
*/
|
|
143
|
-
export function renderBatchProgress(
|
|
144
|
-
progress: FetchProgress[],
|
|
145
|
-
concurrency: number = 8,
|
|
146
|
-
width: number = 80,
|
|
147
|
-
spinnerIndex: number = 0
|
|
148
|
-
): string {
|
|
149
|
-
const lines: string[] = [];
|
|
150
|
-
|
|
151
|
-
// Header
|
|
152
|
-
lines.push(renderBatchProgressHeader(progress, concurrency));
|
|
153
|
-
lines.push("");
|
|
154
|
-
|
|
155
|
-
// Progress items (show up to 10)
|
|
156
|
-
const maxItems = 10;
|
|
157
|
-
const itemsToShow = progress.slice(0, maxItems);
|
|
158
|
-
|
|
159
|
-
for (const item of itemsToShow) {
|
|
160
|
-
lines.push(renderProgressLine(item, width, spinnerIndex));
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
if (progress.length > maxItems) {
|
|
164
|
-
lines.push(` ... and ${progress.length - maxItems} more`);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
return lines.join("\n");
|
|
168
|
-
}
|
package/src/tui/result.ts
DELETED
|
@@ -1,173 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @unipi/web-api — TUI Result Renderer
|
|
3
|
-
*
|
|
4
|
-
* Renders single and batch results for TUI display.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import type { FetchResult, BatchFetchResult, FetchError } from "../engine/types.js";
|
|
8
|
-
|
|
9
|
-
/** Maximum preview lines */
|
|
10
|
-
const PREVIEW_LINES = 7;
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Truncate content for preview.
|
|
14
|
-
*
|
|
15
|
-
* @param content - Content to preview
|
|
16
|
-
* @param maxLines - Maximum lines
|
|
17
|
-
* @returns Preview string
|
|
18
|
-
*/
|
|
19
|
-
function truncatePreview(content: string, maxLines: number = PREVIEW_LINES): string {
|
|
20
|
-
const lines = content.split("\n").slice(0, maxLines);
|
|
21
|
-
return lines.join("\n");
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Render a single result for display.
|
|
26
|
-
*
|
|
27
|
-
* @param result - Fetch result
|
|
28
|
-
* @param verbose - Include metadata header
|
|
29
|
-
* @returns Formatted string
|
|
30
|
-
*/
|
|
31
|
-
export function renderSingleResult(
|
|
32
|
-
result: FetchResult,
|
|
33
|
-
verbose: boolean = true
|
|
34
|
-
): string {
|
|
35
|
-
const lines: string[] = [];
|
|
36
|
-
|
|
37
|
-
if (verbose) {
|
|
38
|
-
// Title
|
|
39
|
-
lines.push(`# ${result.title || "Untitled"}`);
|
|
40
|
-
lines.push("");
|
|
41
|
-
|
|
42
|
-
// Metadata
|
|
43
|
-
const meta: string[] = [];
|
|
44
|
-
if (result.author) {
|
|
45
|
-
meta.push(`Author: ${result.author}`);
|
|
46
|
-
}
|
|
47
|
-
if (result.published) {
|
|
48
|
-
meta.push(`Published: ${result.published}`);
|
|
49
|
-
}
|
|
50
|
-
if (result.site) {
|
|
51
|
-
meta.push(`Site: ${result.site}`);
|
|
52
|
-
}
|
|
53
|
-
if (result.language) {
|
|
54
|
-
meta.push(`Language: ${result.language}`);
|
|
55
|
-
}
|
|
56
|
-
if (result.wordCount) {
|
|
57
|
-
meta.push(`Words: ${result.wordCount}`);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
if (meta.length > 0) {
|
|
61
|
-
lines.push(meta.join(" · "));
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// URL
|
|
65
|
-
lines.push(`URL: ${result.url}`);
|
|
66
|
-
if (result.finalUrl !== result.url) {
|
|
67
|
-
lines.push(`Final URL: ${result.finalUrl}`);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
lines.push("");
|
|
71
|
-
lines.push("---");
|
|
72
|
-
lines.push("");
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// Content preview
|
|
76
|
-
const preview = truncatePreview(result.content);
|
|
77
|
-
lines.push(preview);
|
|
78
|
-
|
|
79
|
-
// Expand hint
|
|
80
|
-
if (result.content.split("\n").length > PREVIEW_LINES) {
|
|
81
|
-
lines.push("");
|
|
82
|
-
lines.push(`... [${result.wordCount} words total · Ctrl+O to expand]`);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
return lines.join("\n");
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Render a batch result for display.
|
|
90
|
-
*
|
|
91
|
-
* @param result - Batch fetch result
|
|
92
|
-
* @returns Formatted string
|
|
93
|
-
*/
|
|
94
|
-
export function renderBatchResult(result: BatchFetchResult): string {
|
|
95
|
-
const lines: string[] = [];
|
|
96
|
-
|
|
97
|
-
// Summary header
|
|
98
|
-
lines.push(`# Batch Read Results`);
|
|
99
|
-
lines.push("");
|
|
100
|
-
lines.push(
|
|
101
|
-
`Total: ${result.total} · Succeeded: ${result.succeeded} · Failed: ${result.failed}`
|
|
102
|
-
);
|
|
103
|
-
lines.push("");
|
|
104
|
-
|
|
105
|
-
// Per-item results
|
|
106
|
-
for (let i = 0; i < result.items.length; i++) {
|
|
107
|
-
const item = result.items[i];
|
|
108
|
-
const status = item.status === "done" ? "✓" : "✗";
|
|
109
|
-
|
|
110
|
-
lines.push(`## [${i + 1}/${result.total}] ${status}`);
|
|
111
|
-
|
|
112
|
-
if (item.status === "done") {
|
|
113
|
-
lines.push(`**${item.result.title}**`);
|
|
114
|
-
lines.push(`URL: ${item.result.url}`);
|
|
115
|
-
lines.push(`Words: ${item.result.wordCount}`);
|
|
116
|
-
lines.push("");
|
|
117
|
-
|
|
118
|
-
// Content preview
|
|
119
|
-
const preview = truncatePreview(item.result.content);
|
|
120
|
-
lines.push(preview);
|
|
121
|
-
|
|
122
|
-
if (item.result.content.split("\n").length > PREVIEW_LINES) {
|
|
123
|
-
lines.push("...");
|
|
124
|
-
}
|
|
125
|
-
} else {
|
|
126
|
-
lines.push(`URL: ${item.error.url || "unknown"}`);
|
|
127
|
-
lines.push(`Error: ${item.error.error}`);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
lines.push("");
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
return lines.join("\n");
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Render an error result for display.
|
|
138
|
-
*
|
|
139
|
-
* @param error - Fetch error
|
|
140
|
-
* @returns Formatted string
|
|
141
|
-
*/
|
|
142
|
-
export function renderErrorResult(error: FetchError): string {
|
|
143
|
-
const lines: string[] = [];
|
|
144
|
-
|
|
145
|
-
lines.push(`# Fetch Error`);
|
|
146
|
-
lines.push("");
|
|
147
|
-
lines.push(`**${error.error}**`);
|
|
148
|
-
lines.push("");
|
|
149
|
-
lines.push(`Code: \`${error.code}\``);
|
|
150
|
-
lines.push(`Phase: \`${error.phase}\``);
|
|
151
|
-
|
|
152
|
-
if (error.url) {
|
|
153
|
-
lines.push("");
|
|
154
|
-
lines.push(`URL: ${error.url}`);
|
|
155
|
-
if (error.finalUrl && error.finalUrl !== error.url) {
|
|
156
|
-
lines.push(`Final URL: ${error.finalUrl}`);
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
if (error.statusCode) {
|
|
161
|
-
lines.push("");
|
|
162
|
-
lines.push(
|
|
163
|
-
`HTTP Status: ${error.statusCode}${error.statusText ? ` ${error.statusText}` : ""}`
|
|
164
|
-
);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
if (error.retryable) {
|
|
168
|
-
lines.push("");
|
|
169
|
-
lines.push(`*This error may be retried.*`);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
return lines.join("\n");
|
|
173
|
-
}
|