@xl0/pi-lovely-web 0.1.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/LICENSE +21 -0
- package/README.md +87 -0
- package/extensions/lovely-web/command.ts +177 -0
- package/extensions/lovely-web/config.ts +134 -0
- package/extensions/lovely-web/format.ts +38 -0
- package/extensions/lovely-web/image.ts +167 -0
- package/extensions/lovely-web/index.ts +12 -0
- package/extensions/lovely-web/providers/brave.ts +103 -0
- package/extensions/lovely-web/providers/exa.ts +112 -0
- package/extensions/lovely-web/providers/firecrawl.ts +91 -0
- package/extensions/lovely-web/providers/http.ts +21 -0
- package/extensions/lovely-web/providers/tavily.ts +131 -0
- package/extensions/lovely-web/providers/types.ts +31 -0
- package/extensions/lovely-web/render.ts +23 -0
- package/extensions/lovely-web/tool-impl.ts +95 -0
- package/extensions/lovely-web/tools.ts +182 -0
- package/package.json +54 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { StringEnum } from "@earendil-works/pi-ai"
|
|
2
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"
|
|
3
|
+
import { Text } from "@earendil-works/pi-tui"
|
|
4
|
+
import { Type } from "typebox"
|
|
5
|
+
import { getImageMaxSize, getProvider, isImageEnabled, isImageResizeEnabled, loadConfig } from "./config.js"
|
|
6
|
+
import { asErrorMessage } from "./format.js"
|
|
7
|
+
import { DEFAULT_MAX_IMAGE_BYTES, imageImpl, MAX_IMAGE_BYTES } from "./image.js"
|
|
8
|
+
import { renderTextResult } from "./render.js"
|
|
9
|
+
import { fetchImpl, searchImpl } from "./tool-impl.js"
|
|
10
|
+
|
|
11
|
+
export function registerLovelyWebTools(pi: ExtensionAPI) {
|
|
12
|
+
pi.registerTool({
|
|
13
|
+
name: "web_search",
|
|
14
|
+
label: "Web Search",
|
|
15
|
+
description: "Search the web.",
|
|
16
|
+
promptSnippet: "Use web_search for current web information.",
|
|
17
|
+
promptGuidelines: [
|
|
18
|
+
"Use web_search when the user asks for current web information, discovery, or sources beyond the local workspace.",
|
|
19
|
+
"Use web_fetch after web_search when you need the full content of a specific page."
|
|
20
|
+
],
|
|
21
|
+
parameters: Type.Object({
|
|
22
|
+
query: Type.String({ description: "The web search query." }),
|
|
23
|
+
limit: Type.Optional(
|
|
24
|
+
Type.Integer({
|
|
25
|
+
description: "Maximum number of results to return. Defaults to 5.",
|
|
26
|
+
minimum: 1,
|
|
27
|
+
maximum: 20
|
|
28
|
+
})
|
|
29
|
+
),
|
|
30
|
+
source: Type.Optional(StringEnum(["web", "news", "images"] as const)),
|
|
31
|
+
fetchResult: Type.Optional(
|
|
32
|
+
Type.Boolean({
|
|
33
|
+
description: "Whether to fetch the first result and include markdown. Defaults to true."
|
|
34
|
+
})
|
|
35
|
+
)
|
|
36
|
+
}),
|
|
37
|
+
renderCall(args, theme, context) {
|
|
38
|
+
const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0)
|
|
39
|
+
const bits = [args.source ?? "web", `limit ${args.limit ?? 5}`]
|
|
40
|
+
if (args.fetchResult ?? true) bits.push("fetch first")
|
|
41
|
+
text.setText(
|
|
42
|
+
`${theme.fg("toolTitle", theme.bold("web_search "))}${theme.fg("muted", `"${args.query}"`)} ${theme.fg("dim", `(${bits.join(", ")})`)}`
|
|
43
|
+
)
|
|
44
|
+
return text
|
|
45
|
+
},
|
|
46
|
+
renderResult(result, { expanded, isPartial }, theme) {
|
|
47
|
+
return renderTextResult(result, expanded, theme, isPartial ? "Searching..." : "No results")
|
|
48
|
+
},
|
|
49
|
+
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
50
|
+
try {
|
|
51
|
+
const config = loadConfig(ctx.cwd)
|
|
52
|
+
const searchProvider = getProvider("search", config)
|
|
53
|
+
onUpdate?.({
|
|
54
|
+
content: [{ type: "text", text: `Searching web with ${searchProvider.label} for: ${params.query}` }],
|
|
55
|
+
details: undefined as unknown
|
|
56
|
+
})
|
|
57
|
+
return await searchImpl(config, params, signal, onUpdate)
|
|
58
|
+
} catch (error) {
|
|
59
|
+
return {
|
|
60
|
+
content: [{ type: "text", text: `Web search failed: ${asErrorMessage(error)}` }],
|
|
61
|
+
details: { error: asErrorMessage(error) },
|
|
62
|
+
isError: true
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
pi.registerTool({
|
|
69
|
+
name: "web_fetch",
|
|
70
|
+
label: "Web Fetch",
|
|
71
|
+
description: "Fetch a page as markdown. Metadata is verbose and opt-in.",
|
|
72
|
+
promptSnippet: "Use web_fetch to fetch a URL as markdown.",
|
|
73
|
+
promptGuidelines: [
|
|
74
|
+
"Use web_fetch when you need the full readable markdown content of a known URL.",
|
|
75
|
+
"Prefer web_fetch over bash/curl for web pages because web_fetch returns cleaned markdown suitable for agent context."
|
|
76
|
+
],
|
|
77
|
+
parameters: Type.Object({
|
|
78
|
+
url: Type.String({ description: "The URL to fetch.", format: "uri" }),
|
|
79
|
+
waitFor: Type.Optional(
|
|
80
|
+
Type.Integer({
|
|
81
|
+
description: "Milliseconds to wait before capturing content, useful for JS-heavy pages.",
|
|
82
|
+
minimum: 0
|
|
83
|
+
})
|
|
84
|
+
),
|
|
85
|
+
timeout: Type.Optional(Type.Integer({ description: "Request timeout in milliseconds. Defaults to 30000.", minimum: 1 })),
|
|
86
|
+
includeMetadata: Type.Optional(
|
|
87
|
+
Type.Boolean({
|
|
88
|
+
description:
|
|
89
|
+
"Append verbose page metadata to the markdown output. Defaults to false. Full metadata is always available in details."
|
|
90
|
+
})
|
|
91
|
+
)
|
|
92
|
+
}),
|
|
93
|
+
renderCall(args, theme, context) {
|
|
94
|
+
const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0)
|
|
95
|
+
const bits: string[] = []
|
|
96
|
+
if (args.waitFor !== undefined) bits.push(`wait ${args.waitFor}ms`)
|
|
97
|
+
if (args.timeout !== undefined) bits.push(`timeout ${args.timeout}ms`)
|
|
98
|
+
if (args.includeMetadata) bits.push("metadata")
|
|
99
|
+
const suffix = bits.length ? ` ${theme.fg("dim", `(${bits.join(", ")})`)}` : ""
|
|
100
|
+
text.setText(`${theme.fg("toolTitle", theme.bold("web_fetch "))}${theme.fg("muted", args.url)}${suffix}`)
|
|
101
|
+
return text
|
|
102
|
+
},
|
|
103
|
+
renderResult(result, { expanded, isPartial }, theme) {
|
|
104
|
+
return renderTextResult(result, expanded, theme, isPartial ? "Fetching..." : "No content")
|
|
105
|
+
},
|
|
106
|
+
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
107
|
+
try {
|
|
108
|
+
const config = loadConfig(ctx.cwd)
|
|
109
|
+
const fetchProvider = getProvider("fetch", config)
|
|
110
|
+
onUpdate?.({
|
|
111
|
+
content: [{ type: "text", text: `Fetching page with ${fetchProvider.label}: ${params.url}` }],
|
|
112
|
+
details: undefined as unknown
|
|
113
|
+
})
|
|
114
|
+
return await fetchImpl(config, params, signal, onUpdate)
|
|
115
|
+
} catch (error) {
|
|
116
|
+
return {
|
|
117
|
+
content: [{ type: "text", text: `Web fetch failed: ${asErrorMessage(error)}` }],
|
|
118
|
+
details: { error: asErrorMessage(error) },
|
|
119
|
+
isError: true
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
pi.registerTool({
|
|
126
|
+
name: "web_image",
|
|
127
|
+
label: "Web Image",
|
|
128
|
+
description: "Fetch an image URL and return it as image content for vision-capable models.",
|
|
129
|
+
promptSnippet: "Use web_image to fetch an image URL as image content.",
|
|
130
|
+
promptGuidelines: [
|
|
131
|
+
"Use web_image when you need to inspect a specific image URL with a vision-capable model.",
|
|
132
|
+
"Prefer web_image only for selected images; web pages can contain many irrelevant images."
|
|
133
|
+
],
|
|
134
|
+
parameters: Type.Object({
|
|
135
|
+
url: Type.String({ description: "The image URL to fetch.", format: "uri" }),
|
|
136
|
+
timeout: Type.Optional(Type.Integer({ description: "Request timeout in milliseconds. Defaults to 30000.", minimum: 1 })),
|
|
137
|
+
maxBytes: Type.Optional(
|
|
138
|
+
Type.Integer({
|
|
139
|
+
description: `Maximum image size in bytes. Defaults to ${DEFAULT_MAX_IMAGE_BYTES}; maximum ${MAX_IMAGE_BYTES}.`,
|
|
140
|
+
minimum: 1,
|
|
141
|
+
maximum: MAX_IMAGE_BYTES
|
|
142
|
+
})
|
|
143
|
+
)
|
|
144
|
+
}),
|
|
145
|
+
renderCall(args, theme, context) {
|
|
146
|
+
const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0)
|
|
147
|
+
const bits: string[] = []
|
|
148
|
+
if (args.timeout !== undefined) bits.push(`timeout ${args.timeout}ms`)
|
|
149
|
+
if (args.maxBytes !== undefined) bits.push(`max ${args.maxBytes} bytes`)
|
|
150
|
+
const suffix = bits.length ? ` ${theme.fg("dim", `(${bits.join(", ")})`)}` : ""
|
|
151
|
+
text.setText(`${theme.fg("toolTitle", theme.bold("web_image "))}${theme.fg("muted", args.url)}${suffix}`)
|
|
152
|
+
return text
|
|
153
|
+
},
|
|
154
|
+
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
155
|
+
try {
|
|
156
|
+
const config = loadConfig(ctx.cwd)
|
|
157
|
+
if (!isImageEnabled(config)) throw new Error("web_image is disabled. Enable it via /lovely-web.")
|
|
158
|
+
onUpdate?.({
|
|
159
|
+
content: [{ type: "text", text: `Fetching image: ${params.url}` }],
|
|
160
|
+
details: undefined as unknown
|
|
161
|
+
})
|
|
162
|
+
return await imageImpl(
|
|
163
|
+
{
|
|
164
|
+
url: params.url,
|
|
165
|
+
timeout: params.timeout,
|
|
166
|
+
maxBytes: params.maxBytes,
|
|
167
|
+
resize: isImageResizeEnabled(config),
|
|
168
|
+
maxSize: getImageMaxSize(config)
|
|
169
|
+
},
|
|
170
|
+
signal,
|
|
171
|
+
onUpdate
|
|
172
|
+
)
|
|
173
|
+
} catch (error) {
|
|
174
|
+
return {
|
|
175
|
+
content: [{ type: "text", text: `Web image failed: ${asErrorMessage(error)}` }],
|
|
176
|
+
details: { error: asErrorMessage(error) },
|
|
177
|
+
isError: true
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
})
|
|
182
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@xl0/pi-lovely-web",
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"keywords": [
|
|
5
|
+
"pi-package",
|
|
6
|
+
"firecrawl",
|
|
7
|
+
"exa",
|
|
8
|
+
"tavily",
|
|
9
|
+
"brave search",
|
|
10
|
+
"web search",
|
|
11
|
+
"web fetch"
|
|
12
|
+
],
|
|
13
|
+
"type": "module",
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"homepage": "https://github.com/xl0/pi-lovely-web#readme",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/xl0/pi-lovely-web.git"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"extensions/",
|
|
22
|
+
"README.md",
|
|
23
|
+
"LICENSE"
|
|
24
|
+
],
|
|
25
|
+
"scripts": {
|
|
26
|
+
"typecheck": "tsgo --noEmit",
|
|
27
|
+
"check": "bun run typecheck && bun run biome:check",
|
|
28
|
+
"test": "bun run test/run.ts",
|
|
29
|
+
"test:image": "bun run test/image.ts",
|
|
30
|
+
"test:update": "bun run test/update-references.ts",
|
|
31
|
+
"release": "bun publish --access public",
|
|
32
|
+
"lint": "biome lint .",
|
|
33
|
+
"format": "biome format --write .",
|
|
34
|
+
"format:check": "biome format .",
|
|
35
|
+
"biome:check": "biome check ."
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@biomejs/biome": "^2.4.14",
|
|
39
|
+
"@typescript/native-preview": "^7.0.0-dev.20260502.1",
|
|
40
|
+
"bun-types": "^1.3.13",
|
|
41
|
+
"typescript": "^6.0.3"
|
|
42
|
+
},
|
|
43
|
+
"peerDependencies": {
|
|
44
|
+
"@earendil-works/pi-ai": ">=0.75.4",
|
|
45
|
+
"@earendil-works/pi-coding-agent": ">=0.75.4",
|
|
46
|
+
"@earendil-works/pi-tui": ">=0.75.4",
|
|
47
|
+
"typebox": "*"
|
|
48
|
+
},
|
|
49
|
+
"pi": {
|
|
50
|
+
"extensions": [
|
|
51
|
+
"./extensions"
|
|
52
|
+
]
|
|
53
|
+
}
|
|
54
|
+
}
|