@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.
Files changed (100) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +191 -0
  3. package/configs/opl-browser.json.sample +8 -0
  4. package/configs/opl-footer.json +48 -0
  5. package/configs/opl-footer.json.sample +48 -0
  6. package/configs/opl-input.json +22 -0
  7. package/configs/opl-input.json.sample +22 -0
  8. package/configs/opl-modes.json +291 -0
  9. package/configs/opl-modes.json.sample +285 -0
  10. package/configs/opl-simplebench.json +8 -0
  11. package/configs/opl-simplebench.json.sample +8 -0
  12. package/configs/opl-todo.json +12 -0
  13. package/configs/opl-todo.json.sample +12 -0
  14. package/configs/opl-webaccess.json +31 -0
  15. package/configs/opl-webaccess.json.sample +31 -0
  16. package/extensions/opl-browser/README.md +83 -0
  17. package/extensions/opl-browser/browser.ts +185 -0
  18. package/extensions/opl-browser/config.ts +37 -0
  19. package/extensions/opl-browser/index.ts +89 -0
  20. package/extensions/opl-browser/package-lock.json +60 -0
  21. package/extensions/opl-browser/package.json +10 -0
  22. package/extensions/opl-ctxtrim/README.md +78 -0
  23. package/extensions/opl-ctxtrim/index.ts +151 -0
  24. package/extensions/opl-footer/README.md +193 -0
  25. package/extensions/opl-footer/config.ts +91 -0
  26. package/extensions/opl-footer/git-status.ts +170 -0
  27. package/extensions/opl-footer/icons.ts +49 -0
  28. package/extensions/opl-footer/index.ts +368 -0
  29. package/extensions/opl-footer/segments/caveman.ts +25 -0
  30. package/extensions/opl-footer/segments/chat-mode.ts +24 -0
  31. package/extensions/opl-footer/segments/context.ts +92 -0
  32. package/extensions/opl-footer/segments/cost.ts +25 -0
  33. package/extensions/opl-footer/segments/git.ts +47 -0
  34. package/extensions/opl-footer/segments/helpers.ts +18 -0
  35. package/extensions/opl-footer/segments/index.ts +51 -0
  36. package/extensions/opl-footer/segments/mode-switcher.ts +24 -0
  37. package/extensions/opl-footer/segments/model.ts +22 -0
  38. package/extensions/opl-footer/segments/path.ts +34 -0
  39. package/extensions/opl-footer/segments/pi.ts +11 -0
  40. package/extensions/opl-footer/segments/plan-mode.ts +24 -0
  41. package/extensions/opl-footer/segments/separator.ts +9 -0
  42. package/extensions/opl-footer/segments/session-stats.ts +82 -0
  43. package/extensions/opl-footer/segments/thinking.ts +51 -0
  44. package/extensions/opl-footer/segments/tokens.ts +66 -0
  45. package/extensions/opl-footer/theme.ts +113 -0
  46. package/extensions/opl-footer/types.ts +172 -0
  47. package/extensions/opl-init/README.md +23 -0
  48. package/extensions/opl-init/index.ts +451 -0
  49. package/extensions/opl-input/README.md +83 -0
  50. package/extensions/opl-input/config.ts +137 -0
  51. package/extensions/opl-input/index.ts +284 -0
  52. package/extensions/opl-input/mode-style.ts +38 -0
  53. package/extensions/opl-input/utils.ts +378 -0
  54. package/extensions/opl-modes/README.md +132 -0
  55. package/extensions/opl-modes/config.ts +473 -0
  56. package/extensions/opl-modes/index.ts +1081 -0
  57. package/extensions/opl-modes/menus.ts +60 -0
  58. package/extensions/opl-modes/state.ts +152 -0
  59. package/extensions/opl-modes/types.ts +164 -0
  60. package/extensions/opl-modes/utils.ts +138 -0
  61. package/extensions/opl-questionnaire/README.md +26 -0
  62. package/extensions/opl-questionnaire/index.ts +445 -0
  63. package/extensions/opl-questionnaire/types.ts +55 -0
  64. package/extensions/opl-simplebench/README.md +242 -0
  65. package/extensions/opl-simplebench/artifact.ts +41 -0
  66. package/extensions/opl-simplebench/benchmark.ts +873 -0
  67. package/extensions/opl-simplebench/coding.ts +282 -0
  68. package/extensions/opl-simplebench/index.ts +203 -0
  69. package/extensions/opl-simplebench/llama-server.ts +290 -0
  70. package/extensions/opl-simplebench/metrics.ts +73 -0
  71. package/extensions/opl-simplebench/report.ts +46 -0
  72. package/extensions/opl-simplebench/research.ts +200 -0
  73. package/extensions/opl-simplebench/scoring.ts +48 -0
  74. package/extensions/opl-simplebench/tests.ts +39 -0
  75. package/extensions/opl-simplebench/types.ts +6 -0
  76. package/extensions/opl-simplebench/util/config.ts +170 -0
  77. package/extensions/opl-simplebench/util/debug.ts +32 -0
  78. package/extensions/opl-simplebench/util/format.ts +402 -0
  79. package/extensions/opl-simplebench/util/providers.ts +841 -0
  80. package/extensions/opl-todo/README.md +43 -0
  81. package/extensions/opl-todo/config.ts +71 -0
  82. package/extensions/opl-todo/index.ts +415 -0
  83. package/extensions/opl-webaccess/README.md +47 -0
  84. package/extensions/opl-webaccess/config.ts +55 -0
  85. package/extensions/opl-webaccess/extract.ts +83 -0
  86. package/extensions/opl-webaccess/index.ts +232 -0
  87. package/extensions/opl-webaccess/package-lock.json +345 -0
  88. package/extensions/opl-webaccess/package.json +18 -0
  89. package/extensions/opl-webaccess/pdf.ts +8 -0
  90. package/extensions/opl-webaccess/providers/ddgs.ts +72 -0
  91. package/extensions/opl-webaccess/providers/exa.ts +74 -0
  92. package/extensions/opl-webaccess/providers/gemini.ts +60 -0
  93. package/extensions/opl-webaccess/providers/searxng.ts +86 -0
  94. package/extensions/opl-webaccess/providers/tavily.ts +60 -0
  95. package/extensions/opl-webaccess/search.ts +36 -0
  96. package/extensions/opl-webaccess/storage.ts +51 -0
  97. package/extensions/opl-webaccess/turndown.d.ts +1 -0
  98. package/extensions/opl-webaccess/types.ts +26 -0
  99. package/extensions/opl-webaccess/utils.ts +28 -0
  100. package/package.json +55 -0
@@ -0,0 +1,83 @@
1
+ import { Readability } from "@mozilla/readability";
2
+ import { parseHTML } from "linkedom";
3
+ import TurndownService from "turndown";
4
+ import { extractPdfBuffer } from "./pdf.js";
5
+ import { errorMessage, isAbortError, isPdfUrl, isPdfContentType } from "./utils.js";
6
+ import type { ExtractedContent } from "./types.js";
7
+
8
+ const CONCURRENT_LIMIT = 3;
9
+
10
+ const td = new TurndownService({
11
+ headingStyle: "atx",
12
+ codeBlockStyle: "fenced",
13
+ });
14
+
15
+ export async function fetchAllContent(
16
+ urls: string[],
17
+ signal?: AbortSignal
18
+ ): Promise<ExtractedContent[]> {
19
+ const results: ExtractedContent[] = [];
20
+ for (let i = 0; i < urls.length; i += CONCURRENT_LIMIT) {
21
+ if (signal?.aborted) break;
22
+ const batch = urls.slice(i, i + CONCURRENT_LIMIT);
23
+ const batchResults = await Promise.all(batch.map((url) => fetchOne(url, signal)));
24
+ results.push(...batchResults);
25
+ }
26
+ return results;
27
+ }
28
+
29
+ async function fetchOne(url: string, signal?: AbortSignal): Promise<ExtractedContent> {
30
+ try {
31
+ const response = await fetch(url, {
32
+ signal,
33
+ headers: {
34
+ "User-Agent": "Mozilla/5.0 (compatible; pi-web-access/1.0)",
35
+ Accept: "text/html,application/xhtml+xml,application/pdf,*/*",
36
+ },
37
+ });
38
+
39
+ if (!response.ok) {
40
+ return { url, title: "", content: "", error: `HTTP ${response.status}: ${url}` };
41
+ }
42
+
43
+ const contentType = response.headers.get("content-type") ?? "";
44
+
45
+ if (isPdfUrl(url) || isPdfContentType(contentType)) {
46
+ const buffer = await response.arrayBuffer();
47
+ const content = await extractPdfBuffer(buffer);
48
+ return { url, title: url, content, error: null };
49
+ }
50
+
51
+ const isHtml =
52
+ contentType.includes("text/html") || contentType.includes("application/xhtml");
53
+
54
+ const body = await response.text();
55
+
56
+ if (!isHtml) {
57
+ // Plain text, markdown, JSON, etc. — return as-is
58
+ return { url, title: "", content: body, error: null };
59
+ }
60
+
61
+ const { document } = parseHTML(body);
62
+ let article = null;
63
+ try {
64
+ article = new Readability(document as unknown as Document).parse();
65
+ } catch {
66
+ // Readability failed — fall back to raw turndown
67
+ }
68
+
69
+ if (!article?.content) {
70
+ return { url, title: "", content: td.turndown(body), error: null };
71
+ }
72
+
73
+ return {
74
+ url,
75
+ title: article.title ?? "",
76
+ content: td.turndown(article.content),
77
+ error: null,
78
+ };
79
+ } catch (err) {
80
+ if (isAbortError(err)) return { url, title: "", content: "", error: "Aborted" };
81
+ return { url, title: "", content: "", error: errorMessage(err) };
82
+ }
83
+ }
@@ -0,0 +1,232 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { Type } from "typebox";
3
+ import { searchWeb } from "./search.js";
4
+ import { fetchAllContent } from "./extract.js";
5
+ import {
6
+ generateId,
7
+ storeResult,
8
+ getResult,
9
+ persistResult,
10
+ restoreFromSession,
11
+ MAX_CONTENT_CHARS,
12
+ } from "./storage.js";
13
+ import { truncate, errorMessage } from "./utils.js";
14
+ import type { StoredData } from "./types.js";
15
+
16
+ export default function (pi: ExtensionAPI) {
17
+ pi.on("session_start", async (_event, ctx) => {
18
+ restoreFromSession(ctx);
19
+ });
20
+
21
+ // ── web_search ────────────────────────────────────────────────────────────
22
+
23
+ pi.registerTool({
24
+ name: "web_search",
25
+ label: "Web Search",
26
+ description:
27
+ "Search the web using the configured search provider (gemini, tavily, ddgs, searxng, or exa). Returns a synthesized answer with source citations. Provider is set in ~/.pi/agent/configs/opl-webaccess.json. Use queries (plural) to run multiple searches in parallel.",
28
+ parameters: Type.Object({
29
+ query: Type.Optional(Type.String({ description: "Single search query" })),
30
+ queries: Type.Optional(
31
+ Type.Array(Type.String(), { description: "Multiple search queries run in parallel" })
32
+ ),
33
+ }),
34
+ async execute(_toolCallId, params, signal) {
35
+ const queryList = params.queries ?? (params.query ? [params.query] : null);
36
+ if (!queryList?.length) {
37
+ return {
38
+ content: [{ type: "text", text: "Error: provide either query or queries." }],
39
+ isError: true,
40
+ details: {} as Record<string, unknown>,
41
+ };
42
+ }
43
+
44
+ let results;
45
+ try {
46
+ results = await Promise.all(queryList.map((q) => searchWeb(q, signal)));
47
+ } catch (err) {
48
+ return {
49
+ content: [{ type: "text", text: `Error: ${errorMessage(err)}` }],
50
+ isError: true,
51
+ details: {} as Record<string, unknown>,
52
+ };
53
+ }
54
+
55
+ const id = generateId();
56
+ const data: StoredData = { id, type: "search", timestamp: Date.now(), queries: results };
57
+ storeResult(id, data);
58
+ persistResult(pi, data);
59
+
60
+ const sections = results.map((r) => {
61
+ if (r.error) return `## Query: "${r.query}"\n\nError: ${r.error}`;
62
+ const sources =
63
+ r.results.length > 0
64
+ ? "\n\n**Sources:**\n" +
65
+ r.results.map((s, i) => `${i + 1}. [${s.title}](${s.url})`).join("\n")
66
+ : "";
67
+ return `## Query: "${r.query}"\n\n${r.answer}${sources}`;
68
+ });
69
+
70
+ const body = truncate(sections.join("\n\n---\n\n"), MAX_CONTENT_CHARS);
71
+ return {
72
+ content: [{ type: "text", text: `${body}\n\n*responseId: ${id}*` }],
73
+ details: { responseId: id, queryCount: results.length },
74
+ };
75
+ },
76
+ });
77
+
78
+ // ── fetch_content ─────────────────────────────────────────────────────────
79
+
80
+ pi.registerTool({
81
+ name: "fetch_content",
82
+ label: "Fetch Content",
83
+ description:
84
+ "Fetch one or more URLs and extract readable content as markdown. Supports regular web pages and PDFs. No API key required. Use urls (plural) for multiple URLs.",
85
+ parameters: Type.Object({
86
+ url: Type.Optional(Type.String({ description: "Single URL to fetch" })),
87
+ urls: Type.Optional(
88
+ Type.Array(Type.String(), { description: "Multiple URLs to fetch in parallel (max 3 at a time)" })
89
+ ),
90
+ }),
91
+ async execute(_toolCallId, params, signal) {
92
+ const urlList = params.urls ?? (params.url ? [params.url] : null);
93
+ if (!urlList?.length) {
94
+ return {
95
+ content: [{ type: "text", text: "Error: provide either url or urls." }],
96
+ isError: true,
97
+ details: {} as Record<string, unknown>,
98
+ };
99
+ }
100
+
101
+ let results;
102
+ try {
103
+ results = await fetchAllContent(urlList, signal);
104
+ } catch (err) {
105
+ return {
106
+ content: [{ type: "text", text: `Error: ${errorMessage(err)}` }],
107
+ isError: true,
108
+ details: {} as Record<string, unknown>,
109
+ };
110
+ }
111
+
112
+ const id = generateId();
113
+ const data: StoredData = { id, type: "fetch", timestamp: Date.now(), urls: results };
114
+ storeResult(id, data);
115
+ persistResult(pi, data);
116
+
117
+ const sections = results.map((r) => {
118
+ if (r.error) return `## ${r.url}\n\nError: ${r.error}`;
119
+ const title = r.title ? `**${r.title}**\n\n` : "";
120
+ return `## ${r.url}\n\n${title}${r.content}`;
121
+ });
122
+
123
+ const body = truncate(sections.join("\n\n---\n\n"), MAX_CONTENT_CHARS);
124
+ return {
125
+ content: [{ type: "text", text: `${body}\n\n*responseId: ${id}*` }],
126
+ details: { responseId: id, urlCount: results.length },
127
+ };
128
+ },
129
+ });
130
+
131
+ // ── get_search_content ────────────────────────────────────────────────────
132
+
133
+ pi.registerTool({
134
+ name: "get_search_content",
135
+ label: "Get Search Content",
136
+ description:
137
+ "Retrieve full stored content from a previous web_search or fetch_content call. Use when content was truncated in the original response.",
138
+ parameters: Type.Object({
139
+ responseId: Type.String({
140
+ description: "The responseId returned by web_search or fetch_content",
141
+ }),
142
+ queryIndex: Type.Optional(
143
+ Type.Number({ description: "Index of the query to retrieve (web_search, default: 0)" })
144
+ ),
145
+ urlIndex: Type.Optional(
146
+ Type.Number({ description: "Index of the URL to retrieve (fetch_content, default: 0)" })
147
+ ),
148
+ url: Type.Optional(
149
+ Type.String({ description: "Retrieve content for a specific URL (fetch_content)" })
150
+ ),
151
+ }),
152
+ async execute(_toolCallId, params) {
153
+ const data = getResult(params.responseId);
154
+ if (!data) {
155
+ return {
156
+ content: [
157
+ {
158
+ type: "text",
159
+ text: `No stored content found for responseId: ${params.responseId}. Results expire after 1 hour or when a new session starts.`,
160
+ },
161
+ ],
162
+ isError: true,
163
+ details: {} as Record<string, unknown>,
164
+ };
165
+ }
166
+
167
+ if (data.type === "search") {
168
+ const idx = params.queryIndex ?? 0;
169
+ const result = data.queries?.[idx];
170
+ if (!result) {
171
+ return {
172
+ content: [
173
+ {
174
+ type: "text",
175
+ text: `No query at index ${idx}. This response has ${data.queries?.length ?? 0} quer${data.queries?.length === 1 ? "y" : "ies"}.`,
176
+ },
177
+ ],
178
+ isError: true,
179
+ details: {} as Record<string, unknown>,
180
+ };
181
+ }
182
+ const sources =
183
+ result.results.length > 0
184
+ ? "\n\n**Sources:**\n" +
185
+ result.results.map((s, i) => `${i + 1}. [${s.title}](${s.url})`).join("\n")
186
+ : "";
187
+ return {
188
+ content: [{ type: "text", text: `## Query: "${result.query}"\n\n${result.answer}${sources}` }],
189
+ details: { responseId: params.responseId },
190
+ };
191
+ }
192
+
193
+ if (data.type === "fetch") {
194
+ const result =
195
+ params.url != null
196
+ ? data.urls?.find((u) => u.url === params.url)
197
+ : data.urls?.[params.urlIndex ?? 0];
198
+
199
+ if (!result) {
200
+ return {
201
+ content: [
202
+ {
203
+ type: "text",
204
+ text: `No URL found. This response has ${data.urls?.length ?? 0} URL(s).`,
205
+ },
206
+ ],
207
+ isError: true,
208
+ details: {} as Record<string, unknown>,
209
+ };
210
+ }
211
+ if (result.error) {
212
+ return {
213
+ content: [{ type: "text", text: `Error fetching ${result.url}: ${result.error}` }],
214
+ isError: true,
215
+ details: {} as Record<string, unknown>,
216
+ };
217
+ }
218
+ const title = result.title ? `**${result.title}**\n\n` : "";
219
+ return {
220
+ content: [{ type: "text", text: `## ${result.url}\n\n${title}${result.content}` }],
221
+ details: { responseId: params.responseId },
222
+ };
223
+ }
224
+
225
+ return {
226
+ content: [{ type: "text", text: "Unknown result type." }],
227
+ isError: true,
228
+ details: {},
229
+ };
230
+ },
231
+ });
232
+ }
@@ -0,0 +1,345 @@
1
+ {
2
+ "name": "opl-webaccess",
3
+ "version": "1.0.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "opl-webaccess",
9
+ "version": "1.0.0",
10
+ "license": "ISC",
11
+ "dependencies": {
12
+ "@mozilla/readability": "^0.6.0",
13
+ "linkedom": "^0.18.13",
14
+ "turndown": "^7.2.4",
15
+ "unpdf": "^1.8.0"
16
+ }
17
+ },
18
+ "node_modules/@mixmark-io/domino": {
19
+ "version": "2.2.0",
20
+ "resolved": "https://registry.npmjs.org/@mixmark-io/domino/-/domino-2.2.0.tgz",
21
+ "integrity": "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==",
22
+ "license": "BSD-2-Clause"
23
+ },
24
+ "node_modules/@mozilla/readability": {
25
+ "version": "0.6.0",
26
+ "resolved": "https://registry.npmjs.org/@mozilla/readability/-/readability-0.6.0.tgz",
27
+ "integrity": "sha512-juG5VWh4qAivzTAeMzvY9xs9HY5rAcr2E4I7tiSSCokRFi7XIZCAu92ZkSTsIj1OPceCifL3cpfteP3pDT9/QQ==",
28
+ "license": "Apache-2.0",
29
+ "engines": {
30
+ "node": ">=14.0.0"
31
+ }
32
+ },
33
+ "node_modules/boolbase": {
34
+ "version": "2.0.0",
35
+ "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-2.0.0.tgz",
36
+ "integrity": "sha512-DkVaaQHymRhpYEYo9x1oo7Q7B0Y6KJUsjm3c9eTyFDby4MHLBTwZ6ZDWBel5zrYxj1WsZgC5oLpiz+93MluXeA==",
37
+ "license": "ISC",
38
+ "engines": {
39
+ "node": ">=20.19.0"
40
+ },
41
+ "funding": {
42
+ "type": "github",
43
+ "url": "https://github.com/sponsors/fb55"
44
+ }
45
+ },
46
+ "node_modules/css-select": {
47
+ "version": "7.0.0",
48
+ "resolved": "https://registry.npmjs.org/css-select/-/css-select-7.0.0.tgz",
49
+ "integrity": "sha512-snmjEVXy+1LnwXdxhYvTMj1d9tOh4HxkA1YmoayVBeeyR2C14Pum7fcxJIm4SswYspVy866eYNwlH6xC3/VH5g==",
50
+ "license": "BSD-2-Clause",
51
+ "dependencies": {
52
+ "boolbase": "^2.0.0",
53
+ "css-what": "^8.0.0",
54
+ "domhandler": "^6.0.1",
55
+ "domutils": "^4.0.2",
56
+ "nth-check": "^3.0.1"
57
+ },
58
+ "engines": {
59
+ "node": ">=20.19.0"
60
+ },
61
+ "funding": {
62
+ "type": "github",
63
+ "url": "https://github.com/sponsors/fb55"
64
+ }
65
+ },
66
+ "node_modules/css-what": {
67
+ "version": "8.0.0",
68
+ "resolved": "https://registry.npmjs.org/css-what/-/css-what-8.0.0.tgz",
69
+ "integrity": "sha512-DH0Bqq3DNp5tdOReuNyAA+Ev4Y2GS5FMbZpeTLP6C4CDi0h5nL0BmUPChXw3o/qbHLDWHl49sbNqQVY7bMSDdw==",
70
+ "license": "BSD-2-Clause",
71
+ "engines": {
72
+ "node": ">=20.19.0"
73
+ },
74
+ "funding": {
75
+ "type": "github",
76
+ "url": "https://github.com/sponsors/fb55"
77
+ }
78
+ },
79
+ "node_modules/cssom": {
80
+ "version": "0.5.0",
81
+ "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz",
82
+ "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==",
83
+ "license": "MIT"
84
+ },
85
+ "node_modules/dom-serializer": {
86
+ "version": "3.1.1",
87
+ "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-3.1.1.tgz",
88
+ "integrity": "sha512-4MEa38/QexBob6gFNwu+EGdWvhJ1OKuNwdYY3Y3NyeWDQfnGeDYQUDfIRzWu5B5gsv03so2Uxd28YC6zrsx3Lw==",
89
+ "license": "MIT",
90
+ "dependencies": {
91
+ "domelementtype": "^3.0.0",
92
+ "domhandler": "^6.0.0",
93
+ "entities": "^8.0.0"
94
+ },
95
+ "engines": {
96
+ "node": ">=20.19.0"
97
+ },
98
+ "funding": {
99
+ "type": "github",
100
+ "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
101
+ }
102
+ },
103
+ "node_modules/domelementtype": {
104
+ "version": "3.0.0",
105
+ "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-3.0.0.tgz",
106
+ "integrity": "sha512-umCQid3jKbDmVjx8jGaW7uUykm4DEUeyV21hPxNMo2nV955DhUThwqyOIDtreepP31hl84X7G5U9ZfsWvIB3Pg==",
107
+ "funding": [
108
+ {
109
+ "type": "github",
110
+ "url": "https://github.com/sponsors/fb55"
111
+ }
112
+ ],
113
+ "license": "BSD-2-Clause",
114
+ "engines": {
115
+ "node": ">=20.19.0"
116
+ }
117
+ },
118
+ "node_modules/domhandler": {
119
+ "version": "6.0.1",
120
+ "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-6.0.1.tgz",
121
+ "integrity": "sha512-gYzvtM72ZtxQO0T048kd6HWSbbGCNOUwcnfQ01cqIJ4X2IYKFFHZ5mKvrQETcFXxsRObZulDaKmy//R7TPtsBg==",
122
+ "license": "BSD-2-Clause",
123
+ "dependencies": {
124
+ "domelementtype": "^3.0.0"
125
+ },
126
+ "engines": {
127
+ "node": ">=20.19.0"
128
+ },
129
+ "funding": {
130
+ "type": "github",
131
+ "url": "https://github.com/fb55/domhandler?sponsor=1"
132
+ }
133
+ },
134
+ "node_modules/domutils": {
135
+ "version": "4.0.2",
136
+ "resolved": "https://registry.npmjs.org/domutils/-/domutils-4.0.2.tgz",
137
+ "integrity": "sha512-qI4JLRKnSzqFqr7hAlS5xQDusBCjKSEG4t4+7aNrIQMHBcsC2TGEhuyABJdYkgSewL57PNLYEiibY2iPKhKpaA==",
138
+ "license": "BSD-2-Clause",
139
+ "dependencies": {
140
+ "dom-serializer": "^3.0.0",
141
+ "domelementtype": "^3.0.0",
142
+ "domhandler": "^6.0.0"
143
+ },
144
+ "engines": {
145
+ "node": ">=20.19.0"
146
+ },
147
+ "funding": {
148
+ "type": "github",
149
+ "url": "https://github.com/fb55/domutils?sponsor=1"
150
+ }
151
+ },
152
+ "node_modules/entities": {
153
+ "version": "8.0.0",
154
+ "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz",
155
+ "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==",
156
+ "license": "BSD-2-Clause",
157
+ "engines": {
158
+ "node": ">=20.19.0"
159
+ },
160
+ "funding": {
161
+ "url": "https://github.com/fb55/entities?sponsor=1"
162
+ }
163
+ },
164
+ "node_modules/html-escaper": {
165
+ "version": "3.0.3",
166
+ "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz",
167
+ "integrity": "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==",
168
+ "license": "MIT"
169
+ },
170
+ "node_modules/htmlparser2": {
171
+ "version": "10.1.0",
172
+ "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz",
173
+ "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==",
174
+ "funding": [
175
+ "https://github.com/fb55/htmlparser2?sponsor=1",
176
+ {
177
+ "type": "github",
178
+ "url": "https://github.com/sponsors/fb55"
179
+ }
180
+ ],
181
+ "license": "MIT",
182
+ "dependencies": {
183
+ "domelementtype": "^2.3.0",
184
+ "domhandler": "^5.0.3",
185
+ "domutils": "^3.2.2",
186
+ "entities": "^7.0.1"
187
+ }
188
+ },
189
+ "node_modules/htmlparser2/node_modules/dom-serializer": {
190
+ "version": "2.0.0",
191
+ "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
192
+ "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
193
+ "license": "MIT",
194
+ "dependencies": {
195
+ "domelementtype": "^2.3.0",
196
+ "domhandler": "^5.0.2",
197
+ "entities": "^4.2.0"
198
+ },
199
+ "funding": {
200
+ "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
201
+ }
202
+ },
203
+ "node_modules/htmlparser2/node_modules/dom-serializer/node_modules/entities": {
204
+ "version": "4.5.0",
205
+ "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
206
+ "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
207
+ "license": "BSD-2-Clause",
208
+ "engines": {
209
+ "node": ">=0.12"
210
+ },
211
+ "funding": {
212
+ "url": "https://github.com/fb55/entities?sponsor=1"
213
+ }
214
+ },
215
+ "node_modules/htmlparser2/node_modules/domelementtype": {
216
+ "version": "2.3.0",
217
+ "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
218
+ "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
219
+ "funding": [
220
+ {
221
+ "type": "github",
222
+ "url": "https://github.com/sponsors/fb55"
223
+ }
224
+ ],
225
+ "license": "BSD-2-Clause"
226
+ },
227
+ "node_modules/htmlparser2/node_modules/domhandler": {
228
+ "version": "5.0.3",
229
+ "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
230
+ "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
231
+ "license": "BSD-2-Clause",
232
+ "dependencies": {
233
+ "domelementtype": "^2.3.0"
234
+ },
235
+ "engines": {
236
+ "node": ">= 4"
237
+ },
238
+ "funding": {
239
+ "url": "https://github.com/fb55/domhandler?sponsor=1"
240
+ }
241
+ },
242
+ "node_modules/htmlparser2/node_modules/domutils": {
243
+ "version": "3.2.2",
244
+ "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
245
+ "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
246
+ "license": "BSD-2-Clause",
247
+ "dependencies": {
248
+ "dom-serializer": "^2.0.0",
249
+ "domelementtype": "^2.3.0",
250
+ "domhandler": "^5.0.3"
251
+ },
252
+ "funding": {
253
+ "url": "https://github.com/fb55/domutils?sponsor=1"
254
+ }
255
+ },
256
+ "node_modules/htmlparser2/node_modules/entities": {
257
+ "version": "7.0.1",
258
+ "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
259
+ "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
260
+ "license": "BSD-2-Clause",
261
+ "engines": {
262
+ "node": ">=0.12"
263
+ },
264
+ "funding": {
265
+ "url": "https://github.com/fb55/entities?sponsor=1"
266
+ }
267
+ },
268
+ "node_modules/linkedom": {
269
+ "version": "0.18.13",
270
+ "resolved": "https://registry.npmjs.org/linkedom/-/linkedom-0.18.13.tgz",
271
+ "integrity": "sha512-ES/o9qotMpzpN2MHs+Iq/JcVoOj8Fa5wiQYrTdFpvAnwXL0g66XHHUc9WUMk6nAlBtGsFQ24ne+SYnvnaQ2FSw==",
272
+ "license": "ISC",
273
+ "dependencies": {
274
+ "css-select": "^7.0.0",
275
+ "cssom": "^0.5.0",
276
+ "html-escaper": "^3.0.3",
277
+ "htmlparser2": "^10.1.0",
278
+ "uhyphen": "^0.2.0"
279
+ },
280
+ "engines": {
281
+ "node": ">=16"
282
+ },
283
+ "peerDependencies": {
284
+ "canvas": ">= 2"
285
+ },
286
+ "peerDependenciesMeta": {
287
+ "canvas": {
288
+ "optional": true
289
+ }
290
+ }
291
+ },
292
+ "node_modules/nth-check": {
293
+ "version": "3.0.1",
294
+ "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-3.0.1.tgz",
295
+ "integrity": "sha512-GX0gsdbGVCgnRgbeGaubfjpBXyYRWOOCVeYh08bSQvDZqxz5ndXs1OTfAt/h36G1xvI94YIspsI0sVFqAV9+RQ==",
296
+ "license": "BSD-2-Clause",
297
+ "dependencies": {
298
+ "boolbase": "^2.0.0"
299
+ },
300
+ "engines": {
301
+ "node": ">=20.19.0"
302
+ },
303
+ "funding": {
304
+ "type": "github",
305
+ "url": "https://github.com/fb55/nth-check?sponsor=1"
306
+ }
307
+ },
308
+ "node_modules/turndown": {
309
+ "version": "7.2.4",
310
+ "resolved": "https://registry.npmjs.org/turndown/-/turndown-7.2.4.tgz",
311
+ "integrity": "sha512-I8yFsfRzmzK0WV1pNNOA4A7y4RDfFxPRxb3t+e3ui14qSGOxGtiSP6GjeX+Y6CHb7HYaFj7ECUD7VE5kQMZWGQ==",
312
+ "license": "MIT",
313
+ "dependencies": {
314
+ "@mixmark-io/domino": "^2.2.0"
315
+ },
316
+ "engines": {
317
+ "node": ">=18",
318
+ "npm": ">=9"
319
+ }
320
+ },
321
+ "node_modules/uhyphen": {
322
+ "version": "0.2.0",
323
+ "resolved": "https://registry.npmjs.org/uhyphen/-/uhyphen-0.2.0.tgz",
324
+ "integrity": "sha512-qz3o9CHXmJJPGBdqzab7qAYuW8kQGKNEuoHFYrBwV6hWIMcpAmxDLXojcHfFr9US1Pe6zUswEIJIbLI610fuqA==",
325
+ "license": "ISC"
326
+ },
327
+ "node_modules/unpdf": {
328
+ "version": "1.8.0",
329
+ "resolved": "https://registry.npmjs.org/unpdf/-/unpdf-1.8.0.tgz",
330
+ "integrity": "sha512-jQlkckbe5nKxRHQdbvo9IKHlU6Cjq00UlxxB8lrCIxvS2o5IbAL1wM+4a1Yc3+BIohg9p5AsoaftwO+G4Aq/qQ==",
331
+ "license": "MIT",
332
+ "engines": {
333
+ "node": ">=22"
334
+ },
335
+ "peerDependencies": {
336
+ "@napi-rs/canvas": "^0.1.69 || ^1.0.0"
337
+ },
338
+ "peerDependenciesMeta": {
339
+ "@napi-rs/canvas": {
340
+ "optional": true
341
+ }
342
+ }
343
+ }
344
+ }
345
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "opl-webaccess",
3
+ "version": "1.0.0",
4
+ "description": "Web search and content extraction for pi. Search the web via Gemini AI, fetch and read web pages, and extract text from PDFs — all from within the agent.",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "test": "echo \"Error: no test specified\" && exit 1"
8
+ },
9
+ "keywords": [],
10
+ "author": "",
11
+ "license": "ISC",
12
+ "dependencies": {
13
+ "@mozilla/readability": "^0.6.0",
14
+ "linkedom": "^0.18.13",
15
+ "turndown": "^7.2.4",
16
+ "unpdf": "^1.8.0"
17
+ }
18
+ }
@@ -0,0 +1,8 @@
1
+ import { errorMessage } from "./utils.js";
2
+
3
+ export async function extractPdfBuffer(buffer: ArrayBuffer): Promise<string> {
4
+ const { getDocumentProxy, extractText } = await import("unpdf");
5
+ const pdf = await getDocumentProxy(new Uint8Array(buffer));
6
+ const { text } = await extractText(pdf, { mergePages: true });
7
+ return text ?? "";
8
+ }