@openlines/opl-pi-sht 0.1.14
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 +191 -0
- package/configs/opl-browser.json.sample +8 -0
- package/configs/opl-footer.json +48 -0
- package/configs/opl-footer.json.sample +48 -0
- package/configs/opl-input.json +22 -0
- package/configs/opl-input.json.sample +22 -0
- package/configs/opl-modes.json +291 -0
- package/configs/opl-modes.json.sample +285 -0
- package/configs/opl-simplebench.json +8 -0
- package/configs/opl-simplebench.json.sample +8 -0
- package/configs/opl-todo.json +12 -0
- package/configs/opl-todo.json.sample +12 -0
- package/configs/opl-webaccess.json +31 -0
- package/configs/opl-webaccess.json.sample +31 -0
- package/extensions/opl-browser/README.md +83 -0
- package/extensions/opl-browser/browser.ts +185 -0
- package/extensions/opl-browser/config.ts +37 -0
- package/extensions/opl-browser/index.ts +89 -0
- package/extensions/opl-browser/package-lock.json +60 -0
- package/extensions/opl-browser/package.json +10 -0
- package/extensions/opl-ctxtrim/README.md +78 -0
- package/extensions/opl-ctxtrim/index.ts +151 -0
- package/extensions/opl-footer/README.md +193 -0
- package/extensions/opl-footer/config.ts +91 -0
- package/extensions/opl-footer/git-status.ts +170 -0
- package/extensions/opl-footer/icons.ts +49 -0
- package/extensions/opl-footer/index.ts +368 -0
- package/extensions/opl-footer/segments/caveman.ts +25 -0
- package/extensions/opl-footer/segments/chat-mode.ts +24 -0
- package/extensions/opl-footer/segments/context.ts +92 -0
- package/extensions/opl-footer/segments/cost.ts +25 -0
- package/extensions/opl-footer/segments/git.ts +47 -0
- package/extensions/opl-footer/segments/helpers.ts +18 -0
- package/extensions/opl-footer/segments/index.ts +51 -0
- package/extensions/opl-footer/segments/mode-switcher.ts +24 -0
- package/extensions/opl-footer/segments/model.ts +22 -0
- package/extensions/opl-footer/segments/path.ts +34 -0
- package/extensions/opl-footer/segments/pi.ts +11 -0
- package/extensions/opl-footer/segments/plan-mode.ts +24 -0
- package/extensions/opl-footer/segments/separator.ts +9 -0
- package/extensions/opl-footer/segments/session-stats.ts +82 -0
- package/extensions/opl-footer/segments/thinking.ts +51 -0
- package/extensions/opl-footer/segments/tokens.ts +66 -0
- package/extensions/opl-footer/theme.ts +113 -0
- package/extensions/opl-footer/types.ts +172 -0
- package/extensions/opl-init/README.md +23 -0
- package/extensions/opl-init/index.ts +451 -0
- package/extensions/opl-input/README.md +83 -0
- package/extensions/opl-input/config.ts +137 -0
- package/extensions/opl-input/index.ts +284 -0
- package/extensions/opl-input/mode-style.ts +38 -0
- package/extensions/opl-input/utils.ts +378 -0
- package/extensions/opl-modes/README.md +132 -0
- package/extensions/opl-modes/config.ts +473 -0
- package/extensions/opl-modes/index.ts +1081 -0
- package/extensions/opl-modes/menus.ts +60 -0
- package/extensions/opl-modes/state.ts +152 -0
- package/extensions/opl-modes/types.ts +164 -0
- package/extensions/opl-modes/utils.ts +138 -0
- package/extensions/opl-questionnaire/README.md +26 -0
- package/extensions/opl-questionnaire/index.ts +445 -0
- package/extensions/opl-questionnaire/types.ts +55 -0
- package/extensions/opl-simplebench/README.md +242 -0
- package/extensions/opl-simplebench/artifact.ts +41 -0
- package/extensions/opl-simplebench/benchmark.ts +873 -0
- package/extensions/opl-simplebench/coding.ts +282 -0
- package/extensions/opl-simplebench/index.ts +203 -0
- package/extensions/opl-simplebench/llama-server.ts +290 -0
- package/extensions/opl-simplebench/metrics.ts +73 -0
- package/extensions/opl-simplebench/report.ts +46 -0
- package/extensions/opl-simplebench/research.ts +200 -0
- package/extensions/opl-simplebench/scoring.ts +48 -0
- package/extensions/opl-simplebench/tests.ts +39 -0
- package/extensions/opl-simplebench/types.ts +6 -0
- package/extensions/opl-simplebench/util/config.ts +170 -0
- package/extensions/opl-simplebench/util/debug.ts +32 -0
- package/extensions/opl-simplebench/util/format.ts +402 -0
- package/extensions/opl-simplebench/util/providers.ts +841 -0
- package/extensions/opl-todo/README.md +43 -0
- package/extensions/opl-todo/config.ts +71 -0
- package/extensions/opl-todo/index.ts +415 -0
- package/extensions/opl-webaccess/README.md +47 -0
- package/extensions/opl-webaccess/config.ts +55 -0
- package/extensions/opl-webaccess/extract.ts +83 -0
- package/extensions/opl-webaccess/index.ts +232 -0
- package/extensions/opl-webaccess/package-lock.json +345 -0
- package/extensions/opl-webaccess/package.json +18 -0
- package/extensions/opl-webaccess/pdf.ts +8 -0
- package/extensions/opl-webaccess/providers/ddgs.ts +72 -0
- package/extensions/opl-webaccess/providers/exa.ts +74 -0
- package/extensions/opl-webaccess/providers/gemini.ts +60 -0
- package/extensions/opl-webaccess/providers/searxng.ts +86 -0
- package/extensions/opl-webaccess/providers/tavily.ts +60 -0
- package/extensions/opl-webaccess/search.ts +36 -0
- package/extensions/opl-webaccess/storage.ts +51 -0
- package/extensions/opl-webaccess/turndown.d.ts +1 -0
- package/extensions/opl-webaccess/types.ts +26 -0
- package/extensions/opl-webaccess/utils.ts +28 -0
- package/package.json +55 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { isAbortError } from "../utils.js";
|
|
2
|
+
import type { ProviderConfig } from "../config.js";
|
|
3
|
+
import type { SearchQueryResult, SearchResult } from "../types.js";
|
|
4
|
+
|
|
5
|
+
interface DdgsSearchResult {
|
|
6
|
+
title: string;
|
|
7
|
+
href: string;
|
|
8
|
+
body: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface DdgsApiResponse {
|
|
12
|
+
results: DdgsSearchResult[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function searchDdgs(
|
|
16
|
+
query: string,
|
|
17
|
+
cfg: ProviderConfig,
|
|
18
|
+
signal?: AbortSignal
|
|
19
|
+
): Promise<SearchQueryResult> {
|
|
20
|
+
if (signal?.aborted) return { query, answer: "", results: [], error: "Aborted" };
|
|
21
|
+
|
|
22
|
+
const apiUrl = (cfg.apiUrl ?? "http://127.0.0.1:8000").replace(/\/$/, "");
|
|
23
|
+
const maxResults = cfg.maxResults ?? 5;
|
|
24
|
+
|
|
25
|
+
const params = new URLSearchParams({
|
|
26
|
+
query,
|
|
27
|
+
max_results: String(maxResults),
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
let response: Response;
|
|
31
|
+
try {
|
|
32
|
+
response = await fetch(`${apiUrl}/search/text?${params.toString()}`, { signal });
|
|
33
|
+
} catch (err) {
|
|
34
|
+
if (isAbortError(err)) return { query, answer: "", results: [], error: "Aborted" };
|
|
35
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
36
|
+
if (msg.includes("ECONNREFUSED") || msg.includes("ENOTFOUND")) {
|
|
37
|
+
return {
|
|
38
|
+
query,
|
|
39
|
+
answer: "",
|
|
40
|
+
results: [],
|
|
41
|
+
error: `DDGS API server not reachable at ${apiUrl}. Check that the container is running.`,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
return { query, answer: "", results: [], error: `DDGS API error: ${msg}` };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (!response.ok) {
|
|
48
|
+
return { query, answer: "", results: [], error: `DDGS API error: HTTP ${response.status}` };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let data: DdgsApiResponse;
|
|
52
|
+
try {
|
|
53
|
+
data = (await response.json()) as DdgsApiResponse;
|
|
54
|
+
} catch {
|
|
55
|
+
return { query, answer: "", results: [], error: "DDGS: failed to parse JSON response" };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const raw = data.results ?? [];
|
|
59
|
+
|
|
60
|
+
const results: SearchResult[] = raw.map((r) => ({
|
|
61
|
+
title: r.title,
|
|
62
|
+
url: r.href,
|
|
63
|
+
}));
|
|
64
|
+
|
|
65
|
+
const answer = raw
|
|
66
|
+
.slice(0, 3)
|
|
67
|
+
.map((r) => r.body)
|
|
68
|
+
.filter(Boolean)
|
|
69
|
+
.join(" ");
|
|
70
|
+
|
|
71
|
+
return { query, answer, results, error: null };
|
|
72
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { errorMessage, isAbortError } from "../utils.js";
|
|
2
|
+
import type { ProviderConfig } from "../config.js";
|
|
3
|
+
import type { SearchQueryResult, SearchResult } from "../types.js";
|
|
4
|
+
|
|
5
|
+
interface ExaResult {
|
|
6
|
+
title: string;
|
|
7
|
+
url: string;
|
|
8
|
+
summary?: string;
|
|
9
|
+
highlights?: string[];
|
|
10
|
+
text?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface ExaResponse {
|
|
14
|
+
results: ExaResult[];
|
|
15
|
+
requestId?: string;
|
|
16
|
+
costDollars?: { total: number };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface ExaError {
|
|
20
|
+
error?: { message: string };
|
|
21
|
+
message?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function searchExa(
|
|
25
|
+
query: string,
|
|
26
|
+
cfg: ProviderConfig,
|
|
27
|
+
apiKey: string,
|
|
28
|
+
signal?: AbortSignal
|
|
29
|
+
): Promise<SearchQueryResult> {
|
|
30
|
+
let response: Response;
|
|
31
|
+
try {
|
|
32
|
+
response = await fetch("https://api.exa.ai/search", {
|
|
33
|
+
method: "POST",
|
|
34
|
+
headers: {
|
|
35
|
+
"x-api-key": apiKey,
|
|
36
|
+
"Content-Type": "application/json",
|
|
37
|
+
},
|
|
38
|
+
body: JSON.stringify({
|
|
39
|
+
query,
|
|
40
|
+
numResults: cfg.maxResults ?? 5,
|
|
41
|
+
type: cfg.searchType ?? "auto",
|
|
42
|
+
contents: cfg.includeSummary
|
|
43
|
+
? { summary: { query } }
|
|
44
|
+
: { highlights: { numSentences: 3 } },
|
|
45
|
+
}),
|
|
46
|
+
signal,
|
|
47
|
+
});
|
|
48
|
+
} catch (err) {
|
|
49
|
+
if (isAbortError(err)) return { query, answer: "", results: [], error: "Aborted" };
|
|
50
|
+
return { query, answer: "", results: [], error: errorMessage(err) };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!response.ok) {
|
|
54
|
+
const err = (await response.json().catch(() => ({}))) as ExaError;
|
|
55
|
+
const msg = err.error?.message ?? err.message ?? `HTTP ${response.status}`;
|
|
56
|
+
return { query, answer: "", results: [], error: `Exa API error: ${msg}` };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const data = (await response.json()) as ExaResponse;
|
|
60
|
+
|
|
61
|
+
const results: SearchResult[] = (data.results ?? []).map((r) => ({
|
|
62
|
+
title: r.title || r.url,
|
|
63
|
+
url: r.url,
|
|
64
|
+
}));
|
|
65
|
+
|
|
66
|
+
// Prefer summary, fall back to highlights, then plain text snippets
|
|
67
|
+
const answer = (data.results ?? [])
|
|
68
|
+
.slice(0, 3)
|
|
69
|
+
.map((r) => r.summary ?? r.highlights?.join(" ") ?? r.text ?? "")
|
|
70
|
+
.filter(Boolean)
|
|
71
|
+
.join("\n\n");
|
|
72
|
+
|
|
73
|
+
return { query, answer, results, error: null };
|
|
74
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { errorMessage, isAbortError } from "../utils.js";
|
|
2
|
+
import type { ProviderConfig } from "../config.js";
|
|
3
|
+
import type { SearchQueryResult, SearchResult } from "../types.js";
|
|
4
|
+
|
|
5
|
+
interface GeminiResponse {
|
|
6
|
+
candidates?: Array<{
|
|
7
|
+
content?: {
|
|
8
|
+
parts?: Array<{ text?: string }>;
|
|
9
|
+
};
|
|
10
|
+
groundingMetadata?: {
|
|
11
|
+
groundingChunks?: Array<{
|
|
12
|
+
web?: { uri: string; title: string };
|
|
13
|
+
}>;
|
|
14
|
+
};
|
|
15
|
+
}>;
|
|
16
|
+
error?: { message: string; code: number };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function searchGemini(
|
|
20
|
+
query: string,
|
|
21
|
+
cfg: ProviderConfig,
|
|
22
|
+
apiKey: string,
|
|
23
|
+
signal?: AbortSignal
|
|
24
|
+
): Promise<SearchQueryResult> {
|
|
25
|
+
const base = cfg.baseUrl ?? "https://generativelanguage.googleapis.com/v1beta";
|
|
26
|
+
const model = cfg.model ?? "gemini-2.5-flash-lite";
|
|
27
|
+
|
|
28
|
+
let response: Response;
|
|
29
|
+
try {
|
|
30
|
+
response = await fetch(`${base}/models/${model}:generateContent?key=${apiKey}`, {
|
|
31
|
+
method: "POST",
|
|
32
|
+
headers: { "Content-Type": "application/json" },
|
|
33
|
+
body: JSON.stringify({
|
|
34
|
+
contents: [{ role: "user", parts: [{ text: query }] }],
|
|
35
|
+
tools: [{ google_search: {} }],
|
|
36
|
+
}),
|
|
37
|
+
signal,
|
|
38
|
+
});
|
|
39
|
+
} catch (err) {
|
|
40
|
+
if (isAbortError(err)) return { query, answer: "", results: [], error: "Aborted" };
|
|
41
|
+
return { query, answer: "", results: [], error: errorMessage(err) };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const data = (await response.json()) as GeminiResponse;
|
|
45
|
+
|
|
46
|
+
if (!response.ok) {
|
|
47
|
+
const msg = data.error?.message ?? `HTTP ${response.status}`;
|
|
48
|
+
return { query, answer: "", results: [], error: `Gemini API error: ${msg}` };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const candidate = data.candidates?.[0];
|
|
52
|
+
const answer = candidate?.content?.parts?.map((p) => p.text ?? "").join("") ?? "";
|
|
53
|
+
const chunks = candidate?.groundingMetadata?.groundingChunks ?? [];
|
|
54
|
+
|
|
55
|
+
const results: SearchResult[] = chunks
|
|
56
|
+
.filter((c): c is { web: { uri: string; title: string } } => !!c.web?.uri)
|
|
57
|
+
.map((c) => ({ title: c.web.title || c.web.uri, url: c.web.uri }));
|
|
58
|
+
|
|
59
|
+
return { query, answer, results, error: null };
|
|
60
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { errorMessage, isAbortError } from "../utils.js";
|
|
2
|
+
import type { ProviderConfig } from "../config.js";
|
|
3
|
+
import type { SearchQueryResult, SearchResult } from "../types.js";
|
|
4
|
+
|
|
5
|
+
interface SearxngResult {
|
|
6
|
+
title: string;
|
|
7
|
+
url: string;
|
|
8
|
+
content?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface SearxngResponse {
|
|
12
|
+
results: SearxngResult[];
|
|
13
|
+
error?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function searchSearxng(
|
|
17
|
+
query: string,
|
|
18
|
+
cfg: ProviderConfig,
|
|
19
|
+
signal?: AbortSignal
|
|
20
|
+
): Promise<SearchQueryResult> {
|
|
21
|
+
const instanceUrl = cfg.instanceUrl?.replace(/\/$/, "");
|
|
22
|
+
if (!instanceUrl) {
|
|
23
|
+
return {
|
|
24
|
+
query,
|
|
25
|
+
answer: "",
|
|
26
|
+
results: [],
|
|
27
|
+
error: 'SearXNG: instanceUrl not set in opl-webaccess.json. Set "instanceUrl": "https://your-searxng-instance.org" under the searxng provider.',
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const params = new URLSearchParams({
|
|
32
|
+
q: query,
|
|
33
|
+
format: "json",
|
|
34
|
+
categories: cfg.categories ?? "general",
|
|
35
|
+
safesearch: String(cfg.safeSearch ?? 0),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
let response: Response;
|
|
39
|
+
try {
|
|
40
|
+
response = await fetch(`${instanceUrl}/search?${params.toString()}`, {
|
|
41
|
+
headers: {
|
|
42
|
+
"Accept": "application/json",
|
|
43
|
+
"User-Agent": "Mozilla/5.0 (compatible; pi-web-access/1.0)",
|
|
44
|
+
},
|
|
45
|
+
signal,
|
|
46
|
+
});
|
|
47
|
+
} catch (err) {
|
|
48
|
+
if (isAbortError(err)) return { query, answer: "", results: [], error: "Aborted" };
|
|
49
|
+
return { query, answer: "", results: [], error: errorMessage(err) };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (response.status === 403) {
|
|
53
|
+
return {
|
|
54
|
+
query,
|
|
55
|
+
answer: "",
|
|
56
|
+
results: [],
|
|
57
|
+
error: `SearXNG: instance at ${instanceUrl} has JSON format disabled. Try a different instance or self-host one. See: https://searx.space`,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!response.ok) {
|
|
62
|
+
return { query, answer: "", results: [], error: `SearXNG error: HTTP ${response.status}` };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const data = (await response.json()) as SearxngResponse;
|
|
66
|
+
|
|
67
|
+
if (data.error) {
|
|
68
|
+
return { query, answer: "", results: [], error: `SearXNG error: ${data.error}` };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const capped = (data.results ?? []).slice(0, cfg.maxResults ?? 5);
|
|
72
|
+
|
|
73
|
+
const results: SearchResult[] = capped.map((r) => ({
|
|
74
|
+
title: r.title,
|
|
75
|
+
url: r.url,
|
|
76
|
+
}));
|
|
77
|
+
|
|
78
|
+
// No native answer synthesis — join top snippets
|
|
79
|
+
const answer = capped
|
|
80
|
+
.slice(0, 3)
|
|
81
|
+
.map((r) => r.content)
|
|
82
|
+
.filter(Boolean)
|
|
83
|
+
.join(" ");
|
|
84
|
+
|
|
85
|
+
return { query, answer, results, error: null };
|
|
86
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { errorMessage, isAbortError } from "../utils.js";
|
|
2
|
+
import type { ProviderConfig } from "../config.js";
|
|
3
|
+
import type { SearchQueryResult, SearchResult } from "../types.js";
|
|
4
|
+
|
|
5
|
+
interface TavilyResult {
|
|
6
|
+
title: string;
|
|
7
|
+
url: string;
|
|
8
|
+
content: string;
|
|
9
|
+
score: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface TavilyResponse {
|
|
13
|
+
answer?: string;
|
|
14
|
+
results: TavilyResult[];
|
|
15
|
+
error?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function searchTavily(
|
|
19
|
+
query: string,
|
|
20
|
+
cfg: ProviderConfig,
|
|
21
|
+
apiKey: string,
|
|
22
|
+
signal?: AbortSignal
|
|
23
|
+
): Promise<SearchQueryResult> {
|
|
24
|
+
let response: Response;
|
|
25
|
+
try {
|
|
26
|
+
response = await fetch("https://api.tavily.com/search", {
|
|
27
|
+
method: "POST",
|
|
28
|
+
headers: { "Content-Type": "application/json" },
|
|
29
|
+
body: JSON.stringify({
|
|
30
|
+
api_key: apiKey,
|
|
31
|
+
query,
|
|
32
|
+
max_results: cfg.maxResults ?? 5,
|
|
33
|
+
include_answer: true,
|
|
34
|
+
}),
|
|
35
|
+
signal,
|
|
36
|
+
});
|
|
37
|
+
} catch (err) {
|
|
38
|
+
if (isAbortError(err)) return { query, answer: "", results: [], error: "Aborted" };
|
|
39
|
+
return { query, answer: "", results: [], error: errorMessage(err) };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const data = (await response.json()) as TavilyResponse;
|
|
43
|
+
|
|
44
|
+
if (!response.ok) {
|
|
45
|
+
return { query, answer: "", results: [], error: `Tavily API error: HTTP ${response.status}` };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (data.error) {
|
|
49
|
+
return { query, answer: "", results: [], error: `Tavily API error: ${data.error}` };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const results: SearchResult[] = (data.results ?? []).map((r) => ({
|
|
53
|
+
title: r.title,
|
|
54
|
+
url: r.url,
|
|
55
|
+
}));
|
|
56
|
+
|
|
57
|
+
const answer = data.answer ?? results.map((r) => r.title).join(". ");
|
|
58
|
+
|
|
59
|
+
return { query, answer, results, error: null };
|
|
60
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { loadConfig, getApiKey } from "./config.js";
|
|
2
|
+
import { searchGemini } from "./providers/gemini.js";
|
|
3
|
+
import { searchTavily } from "./providers/tavily.js";
|
|
4
|
+
import { searchDdgs } from "./providers/ddgs.js";
|
|
5
|
+
import { searchSearxng } from "./providers/searxng.js";
|
|
6
|
+
import { searchExa } from "./providers/exa.js";
|
|
7
|
+
import type { SearchQueryResult } from "./types.js";
|
|
8
|
+
|
|
9
|
+
export async function searchWeb(
|
|
10
|
+
query: string,
|
|
11
|
+
signal?: AbortSignal
|
|
12
|
+
): Promise<SearchQueryResult> {
|
|
13
|
+
const config = loadConfig();
|
|
14
|
+
const providerName = config.provider;
|
|
15
|
+
const providerCfg = config.providers?.[providerName] ?? {};
|
|
16
|
+
|
|
17
|
+
switch (providerName) {
|
|
18
|
+
case "gemini":
|
|
19
|
+
return searchGemini(query, providerCfg, getApiKey(providerCfg), signal);
|
|
20
|
+
case "tavily":
|
|
21
|
+
return searchTavily(query, providerCfg, getApiKey(providerCfg), signal);
|
|
22
|
+
case "ddgs":
|
|
23
|
+
return searchDdgs(query, providerCfg, signal);
|
|
24
|
+
case "searxng":
|
|
25
|
+
return searchSearxng(query, providerCfg, signal);
|
|
26
|
+
case "exa":
|
|
27
|
+
return searchExa(query, providerCfg, getApiKey(providerCfg), signal);
|
|
28
|
+
default:
|
|
29
|
+
return {
|
|
30
|
+
query,
|
|
31
|
+
answer: "",
|
|
32
|
+
results: [],
|
|
33
|
+
error: `Unknown search provider: "${providerName}". Valid options: gemini, tavily, ddgs, searxng, exa`,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { StoredData } from "./types.js";
|
|
3
|
+
|
|
4
|
+
const CUSTOM_TYPE = "web-access-results";
|
|
5
|
+
const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
|
|
6
|
+
|
|
7
|
+
export const MAX_CONTENT_CHARS = 30_000;
|
|
8
|
+
|
|
9
|
+
const store = new Map<string, StoredData>();
|
|
10
|
+
|
|
11
|
+
/** Evict expired entries to prevent unbounded growth. */
|
|
12
|
+
function evictExpired(): void {
|
|
13
|
+
const cutoff = Date.now() - CACHE_TTL_MS;
|
|
14
|
+
for (const [id, data] of store) {
|
|
15
|
+
if (data.timestamp < cutoff) store.delete(id);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function generateId(): string {
|
|
20
|
+
return Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function storeResult(id: string, data: StoredData): void {
|
|
24
|
+
store.set(id, data);
|
|
25
|
+
evictExpired();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function getResult(id: string): StoredData | null {
|
|
29
|
+
return store.get(id) ?? null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function clearStore(): void {
|
|
33
|
+
store.clear();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function persistResult(pi: ExtensionAPI, data: StoredData): void {
|
|
37
|
+
pi.appendEntry(CUSTOM_TYPE, data);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function restoreFromSession(ctx: ExtensionContext): void {
|
|
41
|
+
store.clear();
|
|
42
|
+
const now = Date.now();
|
|
43
|
+
for (const entry of ctx.sessionManager.getBranch()) {
|
|
44
|
+
if (entry.type === "custom" && entry.customType === CUSTOM_TYPE) {
|
|
45
|
+
const data = entry.data as StoredData;
|
|
46
|
+
if (data?.id && data?.type && now - data.timestamp < CACHE_TTL_MS) {
|
|
47
|
+
store.set(data.id, data);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
declare module "turndown";
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export interface SearchResult {
|
|
2
|
+
title: string;
|
|
3
|
+
url: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface SearchQueryResult {
|
|
7
|
+
query: string;
|
|
8
|
+
answer: string;
|
|
9
|
+
results: SearchResult[];
|
|
10
|
+
error: string | null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ExtractedContent {
|
|
14
|
+
url: string;
|
|
15
|
+
title: string;
|
|
16
|
+
content: string;
|
|
17
|
+
error: string | null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface StoredData {
|
|
21
|
+
id: string;
|
|
22
|
+
type: "search" | "fetch";
|
|
23
|
+
timestamp: number;
|
|
24
|
+
queries?: SearchQueryResult[];
|
|
25
|
+
urls?: ExtractedContent[];
|
|
26
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export function errorMessage(err: unknown): string {
|
|
2
|
+
return err instanceof Error ? err.message : String(err);
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function isAbortError(err: unknown): boolean {
|
|
6
|
+
const msg = errorMessage(err).toLowerCase();
|
|
7
|
+
return msg.includes("abort") || msg.includes("cancelled");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function truncate(text: string, maxChars: number): string {
|
|
11
|
+
if (text.length <= maxChars) return text;
|
|
12
|
+
return (
|
|
13
|
+
text.slice(0, maxChars) +
|
|
14
|
+
"\n\n[Content truncated. Use get_search_content with the responseId to retrieve the full content.]"
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function isPdfUrl(url: string): boolean {
|
|
19
|
+
try {
|
|
20
|
+
return new URL(url).pathname.toLowerCase().endsWith(".pdf");
|
|
21
|
+
} catch {
|
|
22
|
+
return url.toLowerCase().endsWith(".pdf");
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function isPdfContentType(contentType: string): boolean {
|
|
27
|
+
return contentType.toLowerCase().includes("application/pdf");
|
|
28
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@openlines/opl-pi-sht",
|
|
3
|
+
"version": "0.1.14",
|
|
4
|
+
"description": "Pi coding-agent extension kit: token-efficient context, safe modes, fingerprinted /init, model benchmarking, web access, and browser automation.",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/linellazatin/opl-pi-sht.git"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"extensions/",
|
|
11
|
+
"configs/"
|
|
12
|
+
],
|
|
13
|
+
"keywords": [
|
|
14
|
+
"pi",
|
|
15
|
+
"pi-package",
|
|
16
|
+
"pi-coding-agent",
|
|
17
|
+
"pi-extension",
|
|
18
|
+
"coding-agent",
|
|
19
|
+
"extension"
|
|
20
|
+
],
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"author": "linellazatin",
|
|
23
|
+
"pi": {
|
|
24
|
+
"extensions": [
|
|
25
|
+
"./extensions/*/index.ts"
|
|
26
|
+
]
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@mozilla/readability": "^0.6.0",
|
|
30
|
+
"linkedom": "^0.18.13",
|
|
31
|
+
"playwright": "^1.49.0",
|
|
32
|
+
"turndown": "^7.2.4",
|
|
33
|
+
"unpdf": "^1.8.0"
|
|
34
|
+
},
|
|
35
|
+
"peerDependencies": {
|
|
36
|
+
"@earendil-works/pi-ai": "*",
|
|
37
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
38
|
+
"@earendil-works/pi-tui": "*",
|
|
39
|
+
"typebox": "*"
|
|
40
|
+
},
|
|
41
|
+
"type": "module",
|
|
42
|
+
"scripts": {
|
|
43
|
+
"test": "npm run test:opl-browser && npm run test:opl-footer && npm run test:opl-init && npm run test:opl-input && npm run test:opl-modes && npm run test:opl-questionnaire && npm run test:opl-todo && npm run test:opl-webaccess && npm run test:opl-simplebench && npm run test:opl-ctxtrim",
|
|
44
|
+
"test:opl-browser": "OPL_EXTENSION=opl-browser bun test tests/extension-smoke.test.mjs",
|
|
45
|
+
"test:opl-footer": "bun test tests/opl-footer-helpers.test.mjs && OPL_EXTENSION=opl-footer bun test tests/extension-smoke.test.mjs",
|
|
46
|
+
"test:opl-init": "bun test tests/opl-init-crawl.test.mjs && OPL_EXTENSION=opl-init bun test tests/extension-smoke.test.mjs",
|
|
47
|
+
"test:opl-input": "bun test tests/opl-input-style.test.mjs && OPL_EXTENSION=opl-input bun test tests/extension-smoke.test.mjs",
|
|
48
|
+
"test:opl-modes": "bun test tests/opl-modes-helpers.test.mjs && OPL_EXTENSION=opl-modes bun test tests/extension-smoke.test.mjs",
|
|
49
|
+
"test:opl-questionnaire": "OPL_EXTENSION=opl-questionnaire bun test tests/extension-smoke.test.mjs",
|
|
50
|
+
"test:opl-todo": "bun test tests/opl-todo-config.test.mjs && OPL_EXTENSION=opl-todo bun test tests/extension-smoke.test.mjs",
|
|
51
|
+
"test:opl-webaccess": "bun test tests/opl-webaccess-helpers.test.mjs && OPL_EXTENSION=opl-webaccess bun test tests/extension-smoke.test.mjs",
|
|
52
|
+
"test:opl-simplebench": "bun test tests/opl-simplebench.test.ts && OPL_EXTENSION=opl-simplebench bun test tests/extension-smoke.test.mjs",
|
|
53
|
+
"test:opl-ctxtrim": "bun test tests/opl-ctxtrim.test.ts && OPL_EXTENSION=opl-ctxtrim bun test tests/extension-smoke.test.mjs"
|
|
54
|
+
}
|
|
55
|
+
}
|