@mrclrchtr/supi-web 1.12.0 → 1.13.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 +2 -2
- package/node_modules/@mrclrchtr/supi-core/package.json +2 -1
- package/node_modules/@mrclrchtr/supi-core/src/api.ts +2 -0
- package/node_modules/@mrclrchtr/supi-core/src/footer-registry.ts +57 -0
- package/node_modules/@mrclrchtr/supi-core/src/index.ts +2 -0
- package/node_modules/@mrclrchtr/supi-core/src/registry-utils.ts +8 -1
- package/package.json +2 -3
- package/src/context7-client.ts +93 -9
- package/src/fetch.ts +1 -1
package/README.md
CHANGED
|
@@ -97,6 +97,6 @@ Skip step 1 if you already know the exact Context7 `library_id`.
|
|
|
97
97
|
|
|
98
98
|
## Context7 API key
|
|
99
99
|
|
|
100
|
-
`web_docs_search` and `web_docs_fetch`
|
|
100
|
+
`web_docs_search` and `web_docs_fetch` call the [Context7 REST API](https://context7.com/) directly.
|
|
101
101
|
|
|
102
|
-
|
|
102
|
+
Set `CONTEXT7_API_KEY` in your environment to authenticate with the Context7 API. Without a key, the tools will return an authentication error when called. Get a free API key at https://context7.com/dashboard.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrclrchtr/supi-core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.13.0",
|
|
4
4
|
"description": "SuPi core — shared infrastructure for SuPi extensions (XML context tags, config system)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
"./config": "./src/config.ts",
|
|
45
45
|
"./context": "./src/context.ts",
|
|
46
46
|
"./debug": "./src/debug-registry.ts",
|
|
47
|
+
"./footer-registry": "./src/footer-registry.ts",
|
|
47
48
|
"./llm": "./src/llm.ts",
|
|
48
49
|
"./package.json": "./package.json",
|
|
49
50
|
"./path": "./src/path.ts",
|
|
@@ -13,6 +13,8 @@ export * from "./context.ts";
|
|
|
13
13
|
// biome-ignore lint/performance/noReExportAll: intentional convenience barrel
|
|
14
14
|
export * from "./debug-registry.ts";
|
|
15
15
|
// biome-ignore lint/performance/noReExportAll: intentional convenience barrel
|
|
16
|
+
export * from "./footer-registry.ts";
|
|
17
|
+
// biome-ignore lint/performance/noReExportAll: intentional convenience barrel
|
|
16
18
|
export * from "./llm.ts";
|
|
17
19
|
// biome-ignore lint/performance/noReExportAll: intentional convenience barrel
|
|
18
20
|
export * from "./path.ts";
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// Shared footer contribution registry for SuPi extensions.
|
|
2
|
+
//
|
|
3
|
+
// Extensions register pre-styled text chunks with a placement hint
|
|
4
|
+
// ("stats" for the metrics line, "status" for the extension status line).
|
|
5
|
+
// The custom footer in supi-extras (or PI's built-in footer) reads these
|
|
6
|
+
// contributions and renders them alongside the built-in metrics.
|
|
7
|
+
|
|
8
|
+
import { createRegistry } from "./registry-utils.ts";
|
|
9
|
+
|
|
10
|
+
/** Where the contribution should appear in the footer. */
|
|
11
|
+
export type FooterPlacement = "stats" | "status";
|
|
12
|
+
|
|
13
|
+
/** A single footer contribution registered by an extension. */
|
|
14
|
+
export interface FooterContribution {
|
|
15
|
+
/** Unique key for this contribution. Re-registering with the same key replaces it. */
|
|
16
|
+
key: string;
|
|
17
|
+
/** Which footer line this belongs on. */
|
|
18
|
+
placement: FooterPlacement;
|
|
19
|
+
/**
|
|
20
|
+
* Sort order within the placement (lower values render further left). Default: 100.
|
|
21
|
+
* Priority 0 is reserved for the turn cache-hit part so it stays adjacent to CH.
|
|
22
|
+
*/
|
|
23
|
+
priority?: number;
|
|
24
|
+
/** Return the pre-styled text for this contribution. Called on every render. */
|
|
25
|
+
render: () => string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const registry = createRegistry<FooterContribution>("footer-contributions");
|
|
29
|
+
|
|
30
|
+
function sortByPriority(a: FooterContribution, b: FooterContribution): number {
|
|
31
|
+
return (a.priority ?? 100) - (b.priority ?? 100);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const footerContributions = {
|
|
35
|
+
/** Register or replace a footer contribution. */
|
|
36
|
+
register(contribution: FooterContribution): void {
|
|
37
|
+
registry.register(contribution.key, contribution);
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
/** Remove a contribution (e.g. on session_shutdown or when disabled). */
|
|
41
|
+
unregister(key: string): void {
|
|
42
|
+
registry.unregister(key);
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
/** Get contributions for a specific placement, sorted by priority. */
|
|
46
|
+
getByPlacement(placement: FooterPlacement): FooterContribution[] {
|
|
47
|
+
return registry
|
|
48
|
+
.getAll()
|
|
49
|
+
.filter((c) => c.placement === placement)
|
|
50
|
+
.sort(sortByPriority);
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
/** Remove all contributions (primarily for tests). */
|
|
54
|
+
clear(): void {
|
|
55
|
+
registry.clear();
|
|
56
|
+
},
|
|
57
|
+
};
|
|
@@ -13,6 +13,8 @@ export * from "./context.ts";
|
|
|
13
13
|
// biome-ignore lint/performance/noReExportAll: intentional convenience barrel
|
|
14
14
|
export * from "./debug-registry.ts";
|
|
15
15
|
// biome-ignore lint/performance/noReExportAll: intentional convenience barrel
|
|
16
|
+
export * from "./footer-registry.ts";
|
|
17
|
+
// biome-ignore lint/performance/noReExportAll: intentional convenience barrel
|
|
16
18
|
export * from "./path.ts";
|
|
17
19
|
// biome-ignore lint/performance/noReExportAll: intentional convenience barrel
|
|
18
20
|
export * from "./project.ts";
|
|
@@ -27,7 +27,7 @@ function getGlobalRegistryMap<T>(name: string): Map<string, T> {
|
|
|
27
27
|
*
|
|
28
28
|
* @typeParam T - The value type stored in the registry.
|
|
29
29
|
* @param name - Unique registry name (used to construct the `Symbol.for` key).
|
|
30
|
-
* @returns An object with `register`, `getAll`, and `clear` functions.
|
|
30
|
+
* @returns An object with `register`, `unregister`, `getAll`, and `clear` functions.
|
|
31
31
|
*/
|
|
32
32
|
export function createRegistry<T>(name: string) {
|
|
33
33
|
const getMap = (): Map<string, T> => getGlobalRegistryMap<T>(name);
|
|
@@ -40,6 +40,13 @@ export function createRegistry<T>(name: string) {
|
|
|
40
40
|
getMap().set(id, value);
|
|
41
41
|
},
|
|
42
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Remove a registration by id. No-op if not registered.
|
|
45
|
+
*/
|
|
46
|
+
unregister: (id: string): void => {
|
|
47
|
+
getMap().delete(id);
|
|
48
|
+
},
|
|
49
|
+
|
|
43
50
|
/**
|
|
44
51
|
* Get all registered values in registration order.
|
|
45
52
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrclrchtr/supi-web",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.13.0",
|
|
4
4
|
"description": "SuPi Web extension — fetch web pages as clean Markdown (web_fetch_md) and library docs via Context7 (web_docs_search, web_docs_fetch)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -20,12 +20,11 @@
|
|
|
20
20
|
"README.md"
|
|
21
21
|
],
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@upstash/context7-sdk": "^0.3.0",
|
|
24
23
|
"jsdom": "^29.0.0",
|
|
25
24
|
"@mozilla/readability": "^0.6.0",
|
|
26
25
|
"turndown": "^7.2.0",
|
|
27
26
|
"turndown-plugin-gfm": "^1.0.2",
|
|
28
|
-
"@mrclrchtr/supi-core": "1.
|
|
27
|
+
"@mrclrchtr/supi-core": "1.13.0"
|
|
29
28
|
},
|
|
30
29
|
"bundledDependencies": [
|
|
31
30
|
"@mrclrchtr/supi-core"
|
package/src/context7-client.ts
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Context7 API client —
|
|
2
|
+
* Context7 API client — calls the Context7 REST API directly via fetch(),
|
|
3
|
+
* matching the official Context7 pi extension pattern.
|
|
3
4
|
*
|
|
4
|
-
* API key is read
|
|
5
|
+
* API key is read from CONTEXT7_API_KEY env var. Without a key, requests are
|
|
6
|
+
* sent without an Authorization header; the API will return 401.
|
|
5
7
|
*/
|
|
6
8
|
|
|
7
|
-
|
|
9
|
+
const BASE_URL = "https://context7.com/api";
|
|
8
10
|
|
|
9
|
-
export
|
|
11
|
+
export class Context7Error extends Error {
|
|
12
|
+
constructor(message: string) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.name = "Context7Error";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
10
17
|
|
|
11
18
|
export interface SearchResult {
|
|
12
19
|
id: string;
|
|
@@ -24,11 +31,74 @@ export interface DocSnippet {
|
|
|
24
31
|
source: string;
|
|
25
32
|
}
|
|
26
33
|
|
|
27
|
-
|
|
34
|
+
function authHeaders(): Record<string, string> {
|
|
35
|
+
const apiKey = process.env.CONTEXT7_API_KEY;
|
|
36
|
+
return apiKey ? { Authorization: `Bearer ${apiKey}` } : {};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function parseErrorResponse(response: Response): Promise<string> {
|
|
40
|
+
try {
|
|
41
|
+
const json = (await response.json()) as { message?: string };
|
|
42
|
+
if (json.message) return json.message;
|
|
43
|
+
} catch {
|
|
44
|
+
// JSON parsing failed, fall through to status-based message
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const hasKey = Boolean(process.env.CONTEXT7_API_KEY);
|
|
48
|
+
if (response.status === 429) {
|
|
49
|
+
return hasKey
|
|
50
|
+
? "Rate limited or quota exceeded. Upgrade your plan at https://context7.com/plans for higher limits."
|
|
51
|
+
: "Rate limited or quota exceeded. Create a free API key at https://context7.com/dashboard for higher limits.";
|
|
52
|
+
}
|
|
53
|
+
if (response.status === 404) {
|
|
54
|
+
return "The library you are trying to access does not exist. Please try with a different library ID.";
|
|
55
|
+
}
|
|
56
|
+
if (response.status === 401) {
|
|
57
|
+
return "Invalid API key. Please check your API key. API keys should start with 'ctx7sk' prefix.";
|
|
58
|
+
}
|
|
59
|
+
return `Request failed with status ${response.status}. Please try again later.`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
interface ApiSearchResult {
|
|
63
|
+
id: string;
|
|
64
|
+
title: string;
|
|
65
|
+
description: string;
|
|
66
|
+
totalSnippets: number;
|
|
67
|
+
trustScore: number;
|
|
68
|
+
benchmarkScore: number;
|
|
69
|
+
versions?: string[];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
interface ApiSearchResponse {
|
|
73
|
+
error?: string;
|
|
74
|
+
results: ApiSearchResult[];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function mapSearchResult(r: ApiSearchResult): SearchResult {
|
|
78
|
+
return {
|
|
79
|
+
id: r.id,
|
|
80
|
+
name: r.title,
|
|
81
|
+
description: r.description,
|
|
82
|
+
totalSnippets: r.totalSnippets,
|
|
83
|
+
trustScore: r.trustScore,
|
|
84
|
+
benchmarkScore: r.benchmarkScore,
|
|
85
|
+
versions: r.versions,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
28
88
|
|
|
29
89
|
export async function searchLibrary(query: string, libraryName: string): Promise<SearchResult[]> {
|
|
30
|
-
const
|
|
31
|
-
|
|
90
|
+
const url = new URL(`${BASE_URL}/v2/libs/search`);
|
|
91
|
+
url.searchParams.set("query", query);
|
|
92
|
+
url.searchParams.set("libraryName", libraryName);
|
|
93
|
+
|
|
94
|
+
const response = await fetch(url, { headers: authHeaders() });
|
|
95
|
+
|
|
96
|
+
if (!response.ok) {
|
|
97
|
+
throw new Context7Error(await parseErrorResponse(response));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const data = (await response.json()) as ApiSearchResponse;
|
|
101
|
+
return (data.results ?? []).map(mapSearchResult);
|
|
32
102
|
}
|
|
33
103
|
|
|
34
104
|
export async function getContext(
|
|
@@ -36,6 +106,20 @@ export async function getContext(
|
|
|
36
106
|
libraryId: string,
|
|
37
107
|
raw?: boolean,
|
|
38
108
|
): Promise<string | DocSnippet[]> {
|
|
39
|
-
const
|
|
40
|
-
|
|
109
|
+
const url = new URL(`${BASE_URL}/v2/context`);
|
|
110
|
+
url.searchParams.set("query", query);
|
|
111
|
+
url.searchParams.set("libraryId", libraryId);
|
|
112
|
+
|
|
113
|
+
const response = await fetch(url, { headers: authHeaders() });
|
|
114
|
+
|
|
115
|
+
if (!response.ok) {
|
|
116
|
+
throw new Context7Error(await parseErrorResponse(response));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (raw) {
|
|
120
|
+
const json = (await response.json()) as DocSnippet[];
|
|
121
|
+
return json;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return response.text();
|
|
41
125
|
}
|
package/src/fetch.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* HTTP fetching with content negotiation and Markdown sniffing.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
// biome-ignore lint/
|
|
5
|
+
// biome-ignore lint/style/noExcessiveLinesPerFile: expanded guessLanguage map pushes past the threshold; nursery rule, not stable
|
|
6
6
|
const USER_AGENT = "Mozilla/5.0 (compatible; supi-web/1.0; +https://github.com/mrclrchtr/supi)";
|
|
7
7
|
const ACCEPT_SIBLING = "text/markdown,text/plain;q=0.9,*/*;q=0.1";
|
|
8
8
|
const DEFAULT_TIMEOUT_MS = 30_000;
|