@mrclrchtr/supi-web 11.4.0 → 12.0.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/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## [12.0.0]
4
+
5
+ - **Breaking:** Web tool parameters are camelCase, like every other SuPi tool. `web_fetch_md`: `output_mode` → `outputMode`, `fetch_mode` → `fetchMode`, `abs_links` → `absLinks`, `timeout_ms` → `timeoutMs`. `web_docs_search`: `library_name` → `libraryName`. `web_docs_fetch`: `library_id` → `libraryId`. Values such as `firecrawl_fallback` are unchanged. Update codemode scripts and saved prompts that pass the old names; they are now rejected as unknown fields.
6
+ - Codemode now lists the Web tools under a `web` namespace heading, so scripts can find them with `describeNamespace("web")` or `searchTools(query, { namespace: "web" })`. Every Web tool now declares audited `readOnlyHint`, `destructiveHint`, `idempotentHint`, and `openWorldHint` annotations, which permission extensions can read from `pi.getAllTools()`. They are read-only and open-world ([#582](https://github.com/mrclrchtr/supi/issues/582)).
7
+
3
8
  ## [11.4.0]
4
9
 
5
10
  - Guides agents toward the library documentation tools for library and framework usage questions, while keeping web search available for issues, release notes, and news.
package/README.md CHANGED
@@ -63,12 +63,12 @@ Results describe the evidence returned by the provider, not exhaustive coverage
63
63
 
64
64
  ```javascript
65
65
  const libraries = await tools.web_docs_search({
66
- library_name: "react",
66
+ libraryName: "react",
67
67
  query: "effect cleanup",
68
68
  });
69
69
  if (libraries.results.length > 0) {
70
70
  const docs = await tools.web_docs_fetch({
71
- library_id: libraries.results[0].id,
71
+ libraryId: libraries.results[0].id,
72
72
  query: "effect cleanup",
73
73
  raw: true,
74
74
  });
@@ -84,7 +84,7 @@ HTTP fetch failures include the requested URL and response status in the error m
84
84
  ```javascript
85
85
  for (const url of urls) {
86
86
  try {
87
- const page = await tools.web_fetch_md({ url, output_mode: "file" });
87
+ const page = await tools.web_fetch_md({ url, outputMode: "file" });
88
88
  text({ url, filePath: page.filePath });
89
89
  } catch (error) {
90
90
  text({ url, error: String(error) });
@@ -117,7 +117,7 @@ Search results are provider-reported source material. A source date can be a pub
117
117
 
118
118
  It follows redirects and, for HTML, removes scripts and styles while preserving headings, links, lists, code blocks, and tables.
119
119
 
120
- Native Markdown is returned unchanged, including relative links and code examples. For HTML conversion, `abs_links` defaults to `true`; set it to `false` to preserve original link and image destinations. Unsafe URL schemes are removed in either HTML mode.
120
+ Native Markdown is returned unchanged, including relative links and code examples. For HTML conversion, `absLinks` defaults to `true`; set it to `false` to preserve original link and image destinations. Unsafe URL schemes are removed in either HTML mode.
121
121
 
122
122
  ### Optional Firecrawl retrieval
123
123
 
@@ -125,7 +125,7 @@ Direct fetching remains the default and needs no API key. To allow Firecrawl, op
125
125
 
126
126
  Export `FIRECRAWL_API_KEY` before starting Pi. The key stays in the environment, not settings. Enabling without a key saves the preference and shows a warning; direct fetching remains available.
127
127
 
128
- The agent can select one `fetch_mode`:
128
+ The agent can select one `fetchMode`:
129
129
 
130
130
  | Mode | Behavior |
131
131
  |---|---|
@@ -140,19 +140,19 @@ For a public page blocked with HTTP 403:
140
140
  ```javascript
141
141
  const page = await tools.web_fetch_md({
142
142
  url: "https://example.com/article",
143
- fetch_mode: "firecrawl_fallback",
144
- output_mode: "file",
143
+ fetchMode: "firecrawl_fallback",
144
+ outputMode: "file",
145
145
  });
146
146
  return page;
147
147
  ```
148
148
 
149
- For JS-rendered content, select `fetch_mode: "firecrawl"` instead. The key is read only when Firecrawl is actually needed and is not returned in results. Firecrawl receives the requested URL and may consume credits, including for cached results. The tool requests Markdown only, with no structured-extraction add-ons or client-side retries. Firecrawl manages its own retrieval pipeline.
149
+ For JS-rendered content, select `fetchMode: "firecrawl"` instead. The key is read only when Firecrawl is actually needed and is not returned in results. Firecrawl receives the requested URL and may consume credits, including for cached results. The tool requests Markdown only, with no structured-extraction add-ons or client-side retries. Firecrawl manages its own retrieval pipeline.
150
150
 
151
151
  `firecrawl_fallback` runs only after the direct pipeline ends with HTTP 403—not for authentication errors (401), missing pages, rate limits, network failures, or cancellation. Neither mode retries Firecrawl failures through another provider. Both modes reject credential-bearing URLs, local/reserved hosts, and hosts resolving to non-public addresses before sharing them. Do not select Firecrawl for signed URLs or login-protected, private, or paywalled pages. These checks are not a sandbox or proof that a page is publicly accessible.
152
152
 
153
153
  Firecrawl results retain the requested source URL and include `retrieval.provider: "firecrawl"`, plus `cacheState` and `cachedAt` when supplied. Both inline/file responses disclose retrieval provenance. [Firecrawl caching](https://docs.firecrawl.dev/features/scrape#caching-and-freshness) allows cached content up to two days old by default; missing cache metadata does not establish freshness. Empty, unsuccessful, warning-bearing, and recognizable access-block responses are failures, not usable article content. A null or absent warning is accepted. Firecrawl can still return incomplete or noisy content, so inspect it before relying on it.
154
154
 
155
- `timeout_ms` applies to each direct HTTP request and to the complete Firecrawl attempt, including DNS validation and response reading; it is not a whole-tool deadline. The provider timeout is clamped to its supported 1–300 second range, while the local deadline still enforces shorter budgets. Cancellation is forwarded to Firecrawl. Native/Firecrawl Markdown remains unchanged, so `abs_links` applies only to local HTML conversion.
155
+ `timeoutMs` applies to each direct HTTP request and to the complete Firecrawl attempt, including DNS validation and response reading; it is not a whole-tool deadline. The provider timeout is clamped to its supported 1–300 second range, while the local deadline still enforces shorter budgets. Cancellation is forwarded to Firecrawl. Native/Firecrawl Markdown remains unchanged, so `absLinks` applies only to local HTML conversion.
156
156
 
157
157
  ### Large pages
158
158
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrclrchtr/supi-core",
3
- "version": "11.4.0",
3
+ "version": "12.0.0",
4
4
  "description": "Shared settings, configuration, reporting, and session infrastructure",
5
5
  "license": "MIT",
6
6
  "repository": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrclrchtr/supi-web",
3
- "version": "11.4.0",
3
+ "version": "12.0.0",
4
4
  "description": "Public web pages, web search sources, and focused Context7 docs",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -39,7 +39,7 @@
39
39
  "@mozilla/readability": "^0.6.0",
40
40
  "turndown": "^7.2.0",
41
41
  "turndown-plugin-gfm": "^1.0.2",
42
- "@mrclrchtr/supi-core": "11.4.0"
42
+ "@mrclrchtr/supi-core": "12.0.0"
43
43
  },
44
44
  "bundledDependencies": [
45
45
  "@mrclrchtr/supi-core"
@@ -0,0 +1,12 @@
1
+ import type { ToolNamespace } from "@earendil-works/pi-coding-agent";
2
+
3
+ /** Codemode namespace shared by every `web_*` tool. */
4
+ export const WEB_TOOL_NAMESPACE: ToolNamespace = {
5
+ name: "web",
6
+ description: "Public web search, page fetching, and library documentation lookup.",
7
+ instructions: [
8
+ "For library or framework usage, call web_docs_search for a libraryId, then web_docs_fetch.",
9
+ "Use web_search for current or external sources such as issues, release notes, or news.",
10
+ "Use web_fetch_md to read a known public URL.",
11
+ ].join("\n"),
12
+ };
@@ -13,11 +13,11 @@ export async function runFetch(
13
13
  _ctx: ExtensionContext,
14
14
  ): Promise<FetchResult> {
15
15
  const input = (params ?? {}) as WebDocsFetchInput;
16
- const libraryId = input.library_id?.trim();
16
+ const libraryId = input.libraryId?.trim();
17
17
  const query = input.query?.trim();
18
18
  const raw = Boolean(input.raw);
19
19
 
20
- if (!libraryId) throw new Error("'library_id' parameter is required");
20
+ if (!libraryId) throw new Error("'libraryId' parameter is required");
21
21
  if (!query) throw new Error("'query' parameter is required");
22
22
 
23
23
  onUpdate?.({
@@ -1,5 +1,5 @@
1
1
  export const toolDescription =
2
- "Fetch focused Context7 docs for a known library_id. Search first if the ID is unknown. Markdown is the default; raw=true returns JSON snippets.";
2
+ "Fetch focused Context7 docs for a known libraryId. Search first if the ID is unknown. Markdown is the default; raw=true returns JSON snippets.";
3
3
 
4
4
  export const promptSnippet = "fetch focused Context7 documentation";
5
5
 
@@ -1,4 +1,5 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { WEB_TOOL_NAMESPACE } from "../namespace.ts";
2
3
  import { promptGuidelines, promptSnippet, toolDescription } from "./guidance.ts";
3
4
  import { renderFetchCall, renderFetchResult } from "./render.ts";
4
5
  import { webDocsFetchSpec } from "./spec.ts";
@@ -7,6 +8,7 @@ import { webDocsFetchSpec } from "./spec.ts";
7
8
  export function registerWebDocsFetchTool(pi: ExtensionAPI): void {
8
9
  pi.registerTool({
9
10
  ...webDocsFetchSpec,
11
+ namespace: WEB_TOOL_NAMESPACE,
10
12
  description: toolDescription,
11
13
  promptSnippet,
12
14
  promptGuidelines: [...promptGuidelines],
@@ -6,7 +6,7 @@ import { WEB_DOCS_FETCH_TOOL_NAME, type WebDocsFetchInput } from "./spec.ts";
6
6
  /** Transcript renderer for web_docs_fetch tool calls. */
7
7
  export function renderFetchCall(args: unknown, theme: Theme) {
8
8
  const input = (args ?? {}) as WebDocsFetchInput;
9
- const libraryId = typeof input.library_id === "string" ? input.library_id : "";
9
+ const libraryId = typeof input.libraryId === "string" ? input.libraryId : "";
10
10
  const query = typeof input.query === "string" ? truncatePreview(input.query) : undefined;
11
11
  return renderToolCall(WEB_DOCS_FETCH_TOOL_NAME, libraryId, theme, query);
12
12
  }
@@ -8,7 +8,7 @@ export const WEB_DOCS_FETCH_TOOL_LABEL = "Web Docs Fetch";
8
8
 
9
9
  export const webDocsFetchParameters = Type.Object(
10
10
  {
11
- library_id: Type.String({
11
+ libraryId: Type.String({
12
12
  description: "Context7 ID, for example /react/react.",
13
13
  }),
14
14
  query: Type.String({ description: "Specific docs question" }),
@@ -28,6 +28,12 @@ export type WebDocsFetchInput = Static<typeof webDocsFetchParameters>;
28
28
  export const webDocsFetchSpec = {
29
29
  name: WEB_DOCS_FETCH_TOOL_NAME,
30
30
  label: WEB_DOCS_FETCH_TOOL_LABEL,
31
+ annotations: {
32
+ readOnlyHint: true,
33
+ destructiveHint: false,
34
+ idempotentHint: true,
35
+ openWorldHint: true,
36
+ },
31
37
  parameters: webDocsFetchParameters,
32
38
  outputSchema: webDocsFetchOutputSchema,
33
39
  execute: runFetch,
@@ -18,10 +18,10 @@ export async function runSearch(
18
18
  _ctx: ExtensionContext,
19
19
  ): Promise<SearchResult> {
20
20
  const input = (params ?? {}) as WebDocsSearchInput;
21
- const libraryName = input.library_name?.trim();
21
+ const libraryName = input.libraryName?.trim();
22
22
  const query = input.query?.trim();
23
23
 
24
- if (!libraryName) throw new Error("'library_name' parameter is required");
24
+ if (!libraryName) throw new Error("'libraryName' parameter is required");
25
25
  if (!query) throw new Error("'query' parameter is required");
26
26
 
27
27
  onUpdate?.({
@@ -1,4 +1,5 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { WEB_TOOL_NAMESPACE } from "../namespace.ts";
2
3
  import { promptGuidelines, promptSnippet, toolDescription } from "./guidance.ts";
3
4
  import { renderSearchCall, renderSearchResult } from "./render.ts";
4
5
  import { webDocsSearchSpec } from "./spec.ts";
@@ -7,6 +8,7 @@ import { webDocsSearchSpec } from "./spec.ts";
7
8
  export function registerWebDocsSearchTool(pi: ExtensionAPI): void {
8
9
  pi.registerTool({
9
10
  ...webDocsSearchSpec,
11
+ namespace: WEB_TOOL_NAMESPACE,
10
12
  description: toolDescription,
11
13
  promptSnippet,
12
14
  promptGuidelines: [...promptGuidelines],
@@ -6,7 +6,7 @@ import { WEB_DOCS_SEARCH_TOOL_NAME, type WebDocsSearchInput } from "./spec.ts";
6
6
  /** Transcript renderer for web_docs_search tool calls. */
7
7
  export function renderSearchCall(args: unknown, theme: Theme) {
8
8
  const input = (args ?? {}) as WebDocsSearchInput;
9
- const libraryName = typeof input.library_name === "string" ? input.library_name : "";
9
+ const libraryName = typeof input.libraryName === "string" ? input.libraryName : "";
10
10
  const query = typeof input.query === "string" ? truncatePreview(input.query) : undefined;
11
11
  return renderToolCall(WEB_DOCS_SEARCH_TOOL_NAME, libraryName, theme, query);
12
12
  }
@@ -83,7 +83,7 @@ export function formatSearchResults(
83
83
  const noun = results.length === 1 ? "library" : "libraries";
84
84
  const hiddenNote =
85
85
  hiddenCount > 0
86
- ? [`_${hiddenCount} more omitted; refine \`library_name\` or \`query\` if needed._`, ""]
86
+ ? [`_${hiddenCount} more omitted; refine \`libraryName\` or \`query\` if needed._`, ""]
87
87
  : [];
88
88
 
89
89
  return [
@@ -8,7 +8,7 @@ export const WEB_DOCS_SEARCH_TOOL_LABEL = "Web Docs Search";
8
8
 
9
9
  export const webDocsSearchParameters = Type.Object(
10
10
  {
11
- library_name: Type.String({
11
+ libraryName: Type.String({
12
12
  description: "Library name (e.g. react, next.js, fastapi)",
13
13
  }),
14
14
  query: Type.String({
@@ -24,6 +24,12 @@ export type WebDocsSearchInput = Static<typeof webDocsSearchParameters>;
24
24
  export const webDocsSearchSpec = {
25
25
  name: WEB_DOCS_SEARCH_TOOL_NAME,
26
26
  label: WEB_DOCS_SEARCH_TOOL_LABEL,
27
+ annotations: {
28
+ readOnlyHint: true,
29
+ destructiveHint: false,
30
+ idempotentHint: true,
31
+ openWorldHint: true,
32
+ },
27
33
  parameters: webDocsSearchParameters,
28
34
  outputSchema: webDocsSearchOutputSchema,
29
35
  execute: runSearch,
@@ -41,9 +41,9 @@ async function fetchAsMarkdown(
41
41
  throw new Error(`URL must be http(s): ${url}`);
42
42
  }
43
43
 
44
- const outputMode = input.output_mode ?? "auto";
45
- const absLinks = input.abs_links ?? true;
46
- const timeoutMs = typeof input.timeout_ms === "number" ? input.timeout_ms : 30_000;
44
+ const outputMode = input.outputMode ?? "auto";
45
+ const absLinks = input.absLinks ?? true;
46
+ const timeoutMs = typeof input.timeoutMs === "number" ? input.timeoutMs : 30_000;
47
47
 
48
48
  onUpdate?.({
49
49
  content: [{ type: "text", text: `Fetching ${url}...` }],
@@ -53,7 +53,7 @@ async function fetchAsMarkdown(
53
53
  const result = await fetchPage(url, {
54
54
  timeoutMs,
55
55
  signal,
56
- fetchMode: input.fetch_mode,
56
+ fetchMode: input.fetchMode,
57
57
  firecrawlEnabled: loadWebConfig(configCwd).firecrawlEnabled,
58
58
  });
59
59
  const markdown = await resolveMarkdown(result, absLinks);
@@ -71,7 +71,7 @@ export async function fetchPage(
71
71
  const mode = options.fetchMode ?? "direct";
72
72
  if (!WEB_FETCH_MODES.includes(mode)) {
73
73
  throw new FetchError(
74
- 'Unsupported fetch_mode; select "direct", "firecrawl_fallback", or "firecrawl"',
74
+ 'Unsupported fetchMode; select "direct", "firecrawl_fallback", or "firecrawl"',
75
75
  );
76
76
  }
77
77
  if (mode !== "direct" && !options.firecrawlEnabled) {
@@ -89,7 +89,7 @@ export async function fetchPage(
89
89
  ? ""
90
90
  : "requires Firecrawl enabled in /supi-settings and FIRECRAWL_API_KEY; ";
91
91
  throw new FetchError(
92
- `${error.message}. For a public page, retry with fetch_mode: "firecrawl_fallback" (${setupHint}shares the URL and may consume credits).`,
92
+ `${error.message}. For a public page, retry with fetchMode: "firecrawl_fallback" (${setupHint}shares the URL and may consume credits).`,
93
93
  { status: 403, cause: error },
94
94
  );
95
95
  }
@@ -26,8 +26,8 @@ const OutputModeEnum = Type.Enum(WEB_FETCH_OUTPUT_MODES, {
26
26
  export const webFetchMdParameters = Type.Object(
27
27
  {
28
28
  url: Type.String({ description: "Public http(s) URL" }),
29
- output_mode: Type.Optional(OutputModeEnum),
30
- fetch_mode: Type.Optional(
29
+ outputMode: Type.Optional(OutputModeEnum),
30
+ fetchMode: Type.Optional(
31
31
  Type.Enum(WEB_FETCH_MODES, {
32
32
  type: "string",
33
33
  default: "direct",
@@ -35,13 +35,13 @@ export const webFetchMdParameters = Type.Object(
35
35
  "direct fetches locally; firecrawl_fallback uses Firecrawl only after HTTP 403; firecrawl skips direct fetching for JS-heavy or poorly extracted pages. Firecrawl requires opt-in via /supi-settings and FIRECRAWL_API_KEY, shares the URL and may consume credits. Never use it for signed, login-protected, private, or paywalled URLs.",
36
36
  }),
37
37
  ),
38
- abs_links: Type.Optional(
38
+ absLinks: Type.Optional(
39
39
  Type.Boolean({
40
40
  description: "Absolutize HTML links/images; false preserves original destinations.",
41
41
  default: true,
42
42
  }),
43
43
  ),
44
- timeout_ms: Type.Optional(
44
+ timeoutMs: Type.Optional(
45
45
  Type.Integer({
46
46
  description: "Timeout per direct request and for the complete Firecrawl attempt (ms)",
47
47
  default: 30_000,
@@ -1,4 +1,5 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { WEB_TOOL_NAMESPACE } from "../namespace.ts";
2
3
  import { createWebFetchExecute } from "./execute.ts";
3
4
  import { getWebFetchPromptSurface } from "./guidance.ts";
4
5
  import { renderWebFetchCall, renderWebFetchResult } from "./render.ts";
@@ -12,6 +13,7 @@ export function registerWebFetchMdTool(pi: ExtensionAPI, configCwd?: string): vo
12
13
  const surface = getWebFetchPromptSurface();
13
14
  pi.registerTool({
14
15
  ...webFetchMdSpec,
16
+ namespace: WEB_TOOL_NAMESPACE,
15
17
  ...(configCwd ? { execute: createWebFetchExecute(configCwd) } : {}),
16
18
  description: surface.description,
17
19
  promptSnippet: surface.promptSnippet,
@@ -7,8 +7,8 @@ import { WEB_FETCH_MD_TOOL_NAME, type WebFetchMdInput } from "./spec.ts";
7
7
  export function renderWebFetchCall(args: unknown, theme: Theme) {
8
8
  const input = (args ?? {}) as WebFetchMdInput;
9
9
  const url = typeof input.url === "string" ? input.url : "";
10
- const outputMode = typeof input.output_mode === "string" ? input.output_mode : undefined;
11
- const annotation = [outputMode, input.fetch_mode].filter(Boolean).join(", ");
10
+ const outputMode = typeof input.outputMode === "string" ? input.outputMode : undefined;
11
+ const annotation = [outputMode, input.fetchMode].filter(Boolean).join(", ");
12
12
  return renderToolCall(WEB_FETCH_MD_TOOL_NAME, url, theme, annotation || undefined);
13
13
  }
14
14
 
@@ -17,6 +17,12 @@ export const WEB_FETCH_MD_TOOL_LABEL = "Web Fetch";
17
17
  export const webFetchMdSpec = {
18
18
  name: WEB_FETCH_MD_TOOL_NAME,
19
19
  label: WEB_FETCH_MD_TOOL_LABEL,
20
+ annotations: {
21
+ readOnlyHint: true,
22
+ destructiveHint: false,
23
+ idempotentHint: true,
24
+ openWorldHint: true,
25
+ },
20
26
  parameters: webFetchMdParameters,
21
27
  outputSchema: webFetchMdOutputSchema,
22
28
  execute: runWebFetch,
@@ -1,4 +1,5 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { WEB_TOOL_NAMESPACE } from "../namespace.ts";
2
3
  import { promptGuidelines, promptSnippet, toolDescription } from "./guidance.ts";
3
4
  import { renderWebSearchCall, renderWebSearchResult } from "./render.ts";
4
5
  import { webSearchSpec } from "./spec.ts";
@@ -7,6 +8,7 @@ import { webSearchSpec } from "./spec.ts";
7
8
  export function registerWebSearchTool(pi: ExtensionAPI): void {
8
9
  pi.registerTool({
9
10
  ...webSearchSpec,
11
+ namespace: WEB_TOOL_NAMESPACE,
10
12
  description: toolDescription,
11
13
  promptSnippet,
12
14
  promptGuidelines: [...promptGuidelines],
@@ -14,6 +14,12 @@ export const WEB_SEARCH_TOOL_LABEL = "Web Search";
14
14
  export const webSearchSpec = {
15
15
  name: WEB_SEARCH_TOOL_NAME,
16
16
  label: WEB_SEARCH_TOOL_LABEL,
17
+ annotations: {
18
+ readOnlyHint: true,
19
+ destructiveHint: false,
20
+ idempotentHint: true,
21
+ openWorldHint: true,
22
+ },
17
23
  parameters: webSearchParameters,
18
24
  outputSchema: webSearchOutputSchema,
19
25
  execute: runWebSearch,