@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.
@@ -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
+ }