@mrclrchtr/supi-web 0.1.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/README.md +96 -0
- package/node_modules/@mrclrchtr/supi-core/README.md +90 -0
- package/node_modules/@mrclrchtr/supi-core/package.json +30 -0
- package/node_modules/@mrclrchtr/supi-core/src/config-settings.ts +76 -0
- package/node_modules/@mrclrchtr/supi-core/src/config.ts +186 -0
- package/node_modules/@mrclrchtr/supi-core/src/context-messages.ts +119 -0
- package/node_modules/@mrclrchtr/supi-core/src/context-provider-registry.ts +36 -0
- package/node_modules/@mrclrchtr/supi-core/src/context-tag.ts +31 -0
- package/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +255 -0
- package/node_modules/@mrclrchtr/supi-core/src/index.ts +83 -0
- package/node_modules/@mrclrchtr/supi-core/src/project-roots.ts +170 -0
- package/node_modules/@mrclrchtr/supi-core/src/registry-utils.ts +54 -0
- package/node_modules/@mrclrchtr/supi-core/src/session-utils.ts +29 -0
- package/node_modules/@mrclrchtr/supi-core/src/settings-command.ts +15 -0
- package/node_modules/@mrclrchtr/supi-core/src/settings-registry.ts +41 -0
- package/node_modules/@mrclrchtr/supi-core/src/settings-ui.ts +226 -0
- package/node_modules/@mrclrchtr/supi-core/src/terminal.ts +60 -0
- package/package.json +50 -0
- package/src/context7-client.ts +41 -0
- package/src/convert.ts +219 -0
- package/src/docs.ts +212 -0
- package/src/fetch.ts +416 -0
- package/src/index.ts +14 -0
- package/src/temp-file.ts +21 -0
- package/src/web.ts +129 -0
package/src/web.ts
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SuPi Web extension entry point — registers the `web_fetch_md` tool with pi.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import { Type } from "typebox";
|
|
7
|
+
import { htmlToMarkdown, wrapAsCodeBlock } from "./convert.ts";
|
|
8
|
+
import { FetchError, fetchWithNegotiation, isValidHttpUrl } from "./fetch.ts";
|
|
9
|
+
import { writeTempFile } from "./temp-file.ts";
|
|
10
|
+
|
|
11
|
+
const TOOL_NAME = "web_fetch_md";
|
|
12
|
+
const TOOL_LABEL = "Web Fetch";
|
|
13
|
+
const INLINE_MAX_CHARS = 15_000;
|
|
14
|
+
|
|
15
|
+
const TOOL_DESCRIPTION = `Fetch a web page and convert it to clean Markdown for LLM ingestion.
|
|
16
|
+
|
|
17
|
+
Only accepts real \`http://\` or \`https://\` URLs. If the page is access-controlled (login, paywall, private content), stop and ask the user for an allowed source or exported content.
|
|
18
|
+
|
|
19
|
+
Output modes:
|
|
20
|
+
- \`auto\` (default): returns Markdown inline if ≤${INLINE_MAX_CHARS.toLocaleString()} characters; otherwise writes to a temporary file and returns the path.
|
|
21
|
+
- \`inline\`: always returns Markdown inline.
|
|
22
|
+
- \`file\`: always writes to a temporary file and returns the path.
|
|
23
|
+
|
|
24
|
+
Links and images are absolutized by default. Use \`abs_links: false\` to keep them as-is.`;
|
|
25
|
+
|
|
26
|
+
const PROMPT_SNIPPET =
|
|
27
|
+
"web_fetch_md — fetch a URL and convert it to clean Markdown suitable for LLM ingestion.";
|
|
28
|
+
|
|
29
|
+
const PROMPT_GUIDELINES = [
|
|
30
|
+
"Use web_fetch_md to fetch web pages and convert them to clean Markdown for LLM ingestion.",
|
|
31
|
+
"Only accept real `http://` or `https://` URLs; stop and ask the user for an allowed source if the page is access-controlled.",
|
|
32
|
+
"Prefer `output_mode: auto` (default) so large pages are written to temp files instead of flooding the context window.",
|
|
33
|
+
"Set `abs_links: false` only when relative links are intentional (e.g., local documentation).",
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
const OutputModeEnum = Type.Union(
|
|
37
|
+
[Type.Literal("auto"), Type.Literal("inline"), Type.Literal("file")],
|
|
38
|
+
{ default: "auto", description: "Output mode: auto, inline, or file" },
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
export default function webExtension(pi: ExtensionAPI): void {
|
|
42
|
+
pi.registerTool({
|
|
43
|
+
name: TOOL_NAME,
|
|
44
|
+
label: TOOL_LABEL,
|
|
45
|
+
description: TOOL_DESCRIPTION,
|
|
46
|
+
promptSnippet: PROMPT_SNIPPET,
|
|
47
|
+
promptGuidelines: PROMPT_GUIDELINES,
|
|
48
|
+
parameters: Type.Object({
|
|
49
|
+
url: Type.String({ description: "http(s) URL to fetch" }),
|
|
50
|
+
output_mode: Type.Optional(OutputModeEnum),
|
|
51
|
+
abs_links: Type.Optional(
|
|
52
|
+
Type.Boolean({ description: "Absolutize relative links/images", default: true }),
|
|
53
|
+
),
|
|
54
|
+
timeout_ms: Type.Optional(
|
|
55
|
+
Type.Number({ description: "Fetch timeout in milliseconds", default: 30_000 }),
|
|
56
|
+
),
|
|
57
|
+
}),
|
|
58
|
+
execute: runWebFetch,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// biome-ignore lint/complexity/useMaxParams: pi ToolDefinition.execute signature
|
|
63
|
+
async function runWebFetch(
|
|
64
|
+
_toolCallId: string,
|
|
65
|
+
params: Record<string, unknown>,
|
|
66
|
+
_signal: AbortSignal | undefined,
|
|
67
|
+
_onUpdate: unknown,
|
|
68
|
+
_ctx: unknown,
|
|
69
|
+
): Promise<{
|
|
70
|
+
content: { type: "text"; text: string }[];
|
|
71
|
+
details: Record<string, unknown>;
|
|
72
|
+
isError?: boolean;
|
|
73
|
+
}> {
|
|
74
|
+
const url = String(params.url || "").trim();
|
|
75
|
+
if (!isValidHttpUrl(url)) {
|
|
76
|
+
return {
|
|
77
|
+
content: [{ type: "text", text: `Error: URL must be http(s): ${url}` }],
|
|
78
|
+
isError: true,
|
|
79
|
+
details: { invalidUrl: url },
|
|
80
|
+
} as const;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const outputMode = (params.output_mode as "auto" | "inline" | "file" | undefined) ?? "auto";
|
|
84
|
+
const absLinks = (params.abs_links as boolean | undefined) ?? true;
|
|
85
|
+
const timeoutMs = typeof params.timeout_ms === "number" ? params.timeout_ms : 30_000;
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
const result = await fetchWithNegotiation(url, { timeoutMs });
|
|
89
|
+
const markdown = await resolveMarkdown(result, absLinks);
|
|
90
|
+
const lines = markdown.split("\n").length;
|
|
91
|
+
const chars = markdown.length;
|
|
92
|
+
|
|
93
|
+
const useFile = outputMode === "file" || (outputMode === "auto" && chars > INLINE_MAX_CHARS);
|
|
94
|
+
|
|
95
|
+
if (useFile) {
|
|
96
|
+
const filePath = await writeTempFile(markdown, "web-fetch-md", ".md");
|
|
97
|
+
return {
|
|
98
|
+
content: [
|
|
99
|
+
{
|
|
100
|
+
type: "text",
|
|
101
|
+
text: `Content written to ${filePath} (${chars.toLocaleString()} chars, ${lines.toLocaleString()} lines). Use the read tool to access it.`,
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
details: { filePath, chars, lines, url: result.url },
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
content: [{ type: "text", text: markdown }],
|
|
110
|
+
details: { chars, lines, url: result.url },
|
|
111
|
+
};
|
|
112
|
+
} catch (err) {
|
|
113
|
+
const message = err instanceof FetchError ? err.message : `Unexpected error: ${String(err)}`;
|
|
114
|
+
return {
|
|
115
|
+
content: [{ type: "text", text: `Error: ${message}` }],
|
|
116
|
+
isError: true,
|
|
117
|
+
details: { url, error: String(err) },
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function resolveMarkdown(
|
|
123
|
+
result: { isMarkdown: boolean; isPlainText: boolean; text: string; url: string },
|
|
124
|
+
absLinks: boolean,
|
|
125
|
+
): Promise<string> {
|
|
126
|
+
if (result.isMarkdown) return result.text;
|
|
127
|
+
if (result.isPlainText) return wrapAsCodeBlock(result.text, result.url);
|
|
128
|
+
return htmlToMarkdown(result.text, result.url, { absLinks });
|
|
129
|
+
}
|