@oh-my-pi/pi-coding-agent 14.5.12 → 14.5.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/CHANGELOG.md +45 -0
- package/package.json +18 -10
- package/src/cli/jupyter-cli.ts +1 -1
- package/src/commit/pipeline.ts +4 -3
- package/src/config/model-equivalence.ts +49 -16
- package/src/config/model-registry.ts +100 -25
- package/src/config/model-resolver.ts +29 -15
- package/src/config/settings-schema.ts +20 -6
- package/src/config/settings.ts +9 -8
- package/src/config.ts +18 -6
- package/src/eval/backend.ts +43 -0
- package/src/eval/eval.lark +43 -0
- package/src/eval/index.ts +5 -0
- package/src/eval/js/context-manager.ts +717 -0
- package/src/eval/js/executor.ts +131 -0
- package/src/eval/js/index.ts +46 -0
- package/src/eval/js/prelude.ts +2 -0
- package/src/eval/js/prelude.txt +84 -0
- package/src/eval/js/tool-bridge.ts +124 -0
- package/src/eval/parse.ts +337 -0
- package/src/{ipy → eval/py}/executor.ts +2 -180
- package/src/{ipy → eval/py}/gateway-coordinator.ts +2 -2
- package/src/eval/py/index.ts +58 -0
- package/src/{ipy → eval/py}/kernel.ts +9 -45
- package/src/{ipy → eval/py}/prelude.py +39 -227
- package/src/eval/types.ts +48 -0
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +8 -10
- package/src/extensibility/extensions/types.ts +2 -3
- package/src/internal-urls/docs-index.generated.ts +5 -5
- package/src/lsp/client.ts +9 -0
- package/src/lsp/index.ts +395 -0
- package/src/lsp/types.ts +15 -4
- package/src/main.ts +35 -14
- package/src/mcp/manager.ts +22 -0
- package/src/mcp/oauth-flow.ts +1 -1
- package/src/memories/index.ts +1 -1
- package/src/modes/acp/acp-event-mapper.ts +1 -1
- package/src/modes/components/{python-execution.ts → eval-execution.ts} +11 -4
- package/src/modes/components/login-dialog.ts +1 -1
- package/src/modes/components/oauth-selector.ts +2 -1
- package/src/modes/components/tool-execution.ts +3 -4
- package/src/modes/controllers/command-controller.ts +28 -8
- package/src/modes/controllers/input-controller.ts +4 -4
- package/src/modes/controllers/selector-controller.ts +2 -1
- package/src/modes/interactive-mode.ts +4 -5
- package/src/modes/rpc/rpc-client.ts +9 -0
- package/src/modes/rpc/rpc-mode.ts +6 -0
- package/src/modes/rpc/rpc-types.ts +9 -0
- package/src/modes/types.ts +3 -3
- package/src/modes/utils/ui-helpers.ts +2 -2
- package/src/prompts/system/system-prompt.md +3 -3
- package/src/prompts/tools/eval.md +92 -0
- package/src/prompts/tools/lsp.md +7 -3
- package/src/sdk.ts +64 -35
- package/src/session/agent-session.ts +152 -46
- package/src/session/messages.ts +1 -1
- package/src/slash-commands/builtin-registry.ts +1 -1
- package/src/system-prompt.ts +34 -66
- package/src/task/agents.ts +4 -5
- package/src/task/executor.ts +5 -9
- package/src/tools/archive-reader.ts +9 -3
- package/src/tools/browser/launch.ts +22 -0
- package/src/tools/browser/readable.ts +11 -6
- package/src/tools/browser/registry.ts +25 -244
- package/src/tools/browser/render.ts +1 -1
- package/src/tools/browser/tab-protocol.ts +101 -0
- package/src/tools/browser/tab-supervisor.ts +429 -0
- package/src/tools/browser/tab-worker-entry.ts +21 -0
- package/src/tools/browser/tab-worker.ts +1006 -0
- package/src/tools/browser.ts +17 -32
- package/src/tools/checkpoint.ts +2 -2
- package/src/tools/{python.ts → eval.ts} +324 -315
- package/src/tools/exit-plan-mode.ts +1 -1
- package/src/tools/image-gen.ts +2 -2
- package/src/tools/index.ts +62 -100
- package/src/tools/read.ts +0 -6
- package/src/tools/recipe/runners/pkg.ts +34 -32
- package/src/tools/renderers.ts +2 -2
- package/src/tools/resolve.ts +7 -2
- package/src/tools/todo-write.ts +0 -1
- package/src/tools/tool-timeouts.ts +2 -2
- package/src/tools/write.ts +8 -1
- package/src/utils/markit.ts +15 -7
- package/src/utils/tools-manager.ts +5 -5
- package/src/web/scrapers/crossref.ts +3 -3
- package/src/web/scrapers/devto.ts +1 -1
- package/src/web/scrapers/discourse.ts +5 -5
- package/src/web/scrapers/firefox-addons.ts +1 -1
- package/src/web/scrapers/flathub.ts +2 -2
- package/src/web/scrapers/gitlab.ts +1 -1
- package/src/web/scrapers/go-pkg.ts +2 -2
- package/src/web/scrapers/jetbrains-marketplace.ts +1 -1
- package/src/web/scrapers/mastodon.ts +9 -9
- package/src/web/scrapers/mdn.ts +11 -7
- package/src/web/scrapers/pub-dev.ts +1 -1
- package/src/web/scrapers/rawg.ts +3 -3
- package/src/web/scrapers/readthedocs.ts +1 -1
- package/src/web/scrapers/spdx.ts +1 -1
- package/src/web/scrapers/stackoverflow.ts +2 -2
- package/src/web/scrapers/types.ts +53 -39
- package/src/web/scrapers/w3c.ts +1 -1
- package/src/web/search/index.ts +5 -5
- package/src/web/search/provider.ts +121 -39
- package/src/web/search/providers/gemini.ts +4 -4
- package/src/web/search/render.ts +2 -2
- package/src/ipy/modules.ts +0 -144
- package/src/prompts/tools/python.md +0 -57
- package/src/tools/browser/vm.ts +0 -792
- /package/src/{ipy → eval/py}/cancellation.ts +0 -0
- /package/src/{ipy → eval/py}/prelude.ts +0 -0
- /package/src/{ipy → eval/py}/runtime.ts +0 -0
|
@@ -77,12 +77,12 @@ function formatCategory(topic: DiscourseTopic): string | null {
|
|
|
77
77
|
return parts.length ? parts.join(" ") : null;
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
function formatPostBody(post: DiscoursePost): string {
|
|
80
|
+
async function formatPostBody(post: DiscoursePost): Promise<string> {
|
|
81
81
|
const raw = post.raw?.trim();
|
|
82
82
|
if (raw) return raw;
|
|
83
83
|
const cooked = post.cooked?.trim();
|
|
84
84
|
if (!cooked) return "";
|
|
85
|
-
return htmlToBasicMarkdown(cooked);
|
|
85
|
+
return await htmlToBasicMarkdown(cooked);
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
function buildTopicUrl(baseUrl: string, topicId: string): string {
|
|
@@ -168,9 +168,9 @@ export const handleDiscourse: SpecialHandler = async (
|
|
|
168
168
|
md += "\n";
|
|
169
169
|
|
|
170
170
|
const description = topic.excerpt
|
|
171
|
-
? htmlToBasicMarkdown(topic.excerpt)
|
|
171
|
+
? await htmlToBasicMarkdown(topic.excerpt)
|
|
172
172
|
: posts.length
|
|
173
|
-
? formatPostBody(posts[0])
|
|
173
|
+
? await formatPostBody(posts[0])
|
|
174
174
|
: "";
|
|
175
175
|
if (description) {
|
|
176
176
|
md += `## Description\n\n${description}\n\n`;
|
|
@@ -182,7 +182,7 @@ export const handleDiscourse: SpecialHandler = async (
|
|
|
182
182
|
const author = formatAuthor({ name: post.name, username: post.username });
|
|
183
183
|
const date = formatIsoDate(post.created_at);
|
|
184
184
|
const likes = post.like_count ?? 0;
|
|
185
|
-
const content = formatPostBody(post);
|
|
185
|
+
const content = await formatPostBody(post);
|
|
186
186
|
const postLabel = post.post_number != null ? `Post ${post.post_number}` : `Post ${post.id}`;
|
|
187
187
|
|
|
188
188
|
md += `### ${postLabel} - ${author} - ${date} - Likes: ${likes}\n\n`;
|
|
@@ -112,7 +112,7 @@ export const handleFirefoxAddons: SpecialHandler = async (
|
|
|
112
112
|
const name = getLocalizedText(data.name, defaultLocale) ?? slug;
|
|
113
113
|
const summary = getLocalizedText(data.summary, defaultLocale);
|
|
114
114
|
const descriptionRaw = getLocalizedText(data.description, defaultLocale);
|
|
115
|
-
const description = descriptionRaw ? htmlToBasicMarkdown(descriptionRaw) : undefined;
|
|
115
|
+
const description = descriptionRaw ? await htmlToBasicMarkdown(descriptionRaw) : undefined;
|
|
116
116
|
|
|
117
117
|
const authors = (data.authors ?? [])
|
|
118
118
|
.map(author => author.name ?? "")
|
|
@@ -170,7 +170,7 @@ export const handleFlathub: SpecialHandler = async (
|
|
|
170
170
|
}
|
|
171
171
|
|
|
172
172
|
if (app.description) {
|
|
173
|
-
const description = htmlToBasicMarkdown(app.description);
|
|
173
|
+
const description = await htmlToBasicMarkdown(app.description);
|
|
174
174
|
if (description) md += `\n## Description\n\n${description}\n`;
|
|
175
175
|
}
|
|
176
176
|
|
|
@@ -204,7 +204,7 @@ export const handleFlathub: SpecialHandler = async (
|
|
|
204
204
|
md += `${line}\n`;
|
|
205
205
|
|
|
206
206
|
if (release.description) {
|
|
207
|
-
const releaseDesc = htmlToBasicMarkdown(release.description).replace(/\n+/g, " ").trim();
|
|
207
|
+
const releaseDesc = (await htmlToBasicMarkdown(release.description)).replace(/\n+/g, " ").trim();
|
|
208
208
|
if (releaseDesc) md += ` - ${releaseDesc}\n`;
|
|
209
209
|
}
|
|
210
210
|
}
|
|
@@ -259,7 +259,7 @@ async function renderGitLabIssue(
|
|
|
259
259
|
}
|
|
260
260
|
|
|
261
261
|
md += `\n---\n\n## Description\n\n`;
|
|
262
|
-
md += issue.description ? htmlToBasicMarkdown(issue.description) : "*No description*";
|
|
262
|
+
md += issue.description ? await htmlToBasicMarkdown(issue.description) : "*No description*";
|
|
263
263
|
|
|
264
264
|
return { content: md, ok: true };
|
|
265
265
|
}
|
|
@@ -159,7 +159,7 @@ export const handleGoPkg: SpecialHandler = async (
|
|
|
159
159
|
// Get overview paragraph
|
|
160
160
|
const overview = docSection.querySelector(".go-Message");
|
|
161
161
|
if (overview) {
|
|
162
|
-
const overviewMd = htmlToBasicMarkdown(overview.innerHTML);
|
|
162
|
+
const overviewMd = await htmlToBasicMarkdown(overview.innerHTML);
|
|
163
163
|
sections.push(overviewMd);
|
|
164
164
|
sections.push("");
|
|
165
165
|
}
|
|
@@ -172,7 +172,7 @@ export const handleGoPkg: SpecialHandler = async (
|
|
|
172
172
|
const docParts: string[] = [];
|
|
173
173
|
for (let i = 0; i < Math.min(3, paragraphs.length); i++) {
|
|
174
174
|
const p = paragraphs[i];
|
|
175
|
-
const text = htmlToBasicMarkdown(p.innerHTML).trim();
|
|
175
|
+
const text = (await htmlToBasicMarkdown(p.innerHTML)).trim();
|
|
176
176
|
if (text) {
|
|
177
177
|
docParts.push(text);
|
|
178
178
|
}
|
|
@@ -108,7 +108,7 @@ export const handleJetBrainsMarketplace: SpecialHandler = async (
|
|
|
108
108
|
|
|
109
109
|
const vendorName = plugin.vendor?.name ?? plugin.vendor?.publicName;
|
|
110
110
|
const descriptionSource = plugin.description ?? plugin.preview ?? "";
|
|
111
|
-
const description = descriptionSource ? htmlToBasicMarkdown(descriptionSource) : "";
|
|
111
|
+
const description = descriptionSource ? await htmlToBasicMarkdown(descriptionSource) : "";
|
|
112
112
|
const tags = (plugin.tags ?? []).map(tag => tag.name).filter(Boolean) as string[];
|
|
113
113
|
const rating = extractRating(plugin);
|
|
114
114
|
const buildCompatibility = update ? formatBuildCompatibility(update) : null;
|
|
@@ -89,11 +89,11 @@ function formatDate(isoDate: string): string {
|
|
|
89
89
|
/**
|
|
90
90
|
* Format a status/post as markdown
|
|
91
91
|
*/
|
|
92
|
-
function formatStatus(status: MastodonStatus, isReblog = false): string {
|
|
92
|
+
async function formatStatus(status: MastodonStatus, isReblog = false): Promise<string> {
|
|
93
93
|
// Handle reblogs (boosts)
|
|
94
94
|
if (status.reblog && !isReblog) {
|
|
95
95
|
let md = `🔁 **${status.account.display_name || status.account.username}** boosted:\n\n`;
|
|
96
|
-
md += formatStatus(status.reblog, true);
|
|
96
|
+
md += await formatStatus(status.reblog, true);
|
|
97
97
|
return md;
|
|
98
98
|
}
|
|
99
99
|
|
|
@@ -116,7 +116,7 @@ function formatStatus(status: MastodonStatus, isReblog = false): string {
|
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
// Main content (convert HTML to markdown)
|
|
119
|
-
const content = htmlToBasicMarkdown(status.content);
|
|
119
|
+
const content = await htmlToBasicMarkdown(status.content);
|
|
120
120
|
md += `${content}\n\n`;
|
|
121
121
|
|
|
122
122
|
// Poll
|
|
@@ -152,7 +152,7 @@ function formatStatus(status: MastodonStatus, isReblog = false): string {
|
|
|
152
152
|
/**
|
|
153
153
|
* Format an account/profile as markdown
|
|
154
154
|
*/
|
|
155
|
-
function formatAccount(account: MastodonAccount): string {
|
|
155
|
+
async function formatAccount(account: MastodonAccount): Promise<string> {
|
|
156
156
|
let md = `# ${account.display_name || account.username}\n\n`;
|
|
157
157
|
|
|
158
158
|
md += `**@${account.acct}**`;
|
|
@@ -161,7 +161,7 @@ function formatAccount(account: MastodonAccount): string {
|
|
|
161
161
|
|
|
162
162
|
// Bio
|
|
163
163
|
if (account.note) {
|
|
164
|
-
const bio = htmlToBasicMarkdown(account.note);
|
|
164
|
+
const bio = await htmlToBasicMarkdown(account.note);
|
|
165
165
|
if (bio && bio !== account.display_name) {
|
|
166
166
|
md += `${bio}\n\n`;
|
|
167
167
|
}
|
|
@@ -179,7 +179,7 @@ function formatAccount(account: MastodonAccount): string {
|
|
|
179
179
|
if (account.fields && account.fields.length > 0) {
|
|
180
180
|
md += "\n**Profile Fields:**\n";
|
|
181
181
|
for (const field of account.fields) {
|
|
182
|
-
const value = htmlToBasicMarkdown(field.value);
|
|
182
|
+
const value = await htmlToBasicMarkdown(field.value);
|
|
183
183
|
md += `- **${field.name}:** ${value}\n`;
|
|
184
184
|
}
|
|
185
185
|
}
|
|
@@ -228,7 +228,7 @@ export const handleMastodon: SpecialHandler = async (
|
|
|
228
228
|
const status = tryParseJson<MastodonStatus>(result.content);
|
|
229
229
|
if (!status) return null;
|
|
230
230
|
|
|
231
|
-
const md = formatStatus(status);
|
|
231
|
+
const md = await formatStatus(status);
|
|
232
232
|
|
|
233
233
|
return buildResult(md, {
|
|
234
234
|
url,
|
|
@@ -263,7 +263,7 @@ export const handleMastodon: SpecialHandler = async (
|
|
|
263
263
|
signal,
|
|
264
264
|
});
|
|
265
265
|
|
|
266
|
-
let md = formatAccount(account);
|
|
266
|
+
let md = await formatAccount(account);
|
|
267
267
|
|
|
268
268
|
if (statusesResult.ok) {
|
|
269
269
|
const statuses = tryParseJson<MastodonStatus[]>(statusesResult.content);
|
|
@@ -271,7 +271,7 @@ export const handleMastodon: SpecialHandler = async (
|
|
|
271
271
|
md += "\n---\n\n## Recent Posts\n\n";
|
|
272
272
|
for (const status of statuses.slice(0, 5)) {
|
|
273
273
|
md += `### ${formatDate(status.created_at)}\n\n`;
|
|
274
|
-
const content = htmlToBasicMarkdown(status.content);
|
|
274
|
+
const content = await htmlToBasicMarkdown(status.content);
|
|
275
275
|
md += `${content}\n\n`;
|
|
276
276
|
md += `\uD83D\uDCAC ${status.replies_count} \u00B7 \uD83D\uDD01 ${status.reblogs_count} \u00B7 \u2B50 ${status.favourites_count}\n\n`;
|
|
277
277
|
}
|
package/src/web/scrapers/mdn.ts
CHANGED
|
@@ -29,7 +29,7 @@ interface MDNDoc {
|
|
|
29
29
|
/**
|
|
30
30
|
* Convert MDN body sections to markdown
|
|
31
31
|
*/
|
|
32
|
-
function convertMDNBody(sections: MDNSection[]): string {
|
|
32
|
+
async function convertMDNBody(sections: MDNSection[]): Promise<string> {
|
|
33
33
|
const parts: string[] = [];
|
|
34
34
|
|
|
35
35
|
for (const section of sections) {
|
|
@@ -38,7 +38,7 @@ function convertMDNBody(sections: MDNSection[]): string {
|
|
|
38
38
|
switch (type) {
|
|
39
39
|
case "prose":
|
|
40
40
|
if (value.content) {
|
|
41
|
-
const markdown = htmlToBasicMarkdown(value.content);
|
|
41
|
+
const markdown = await htmlToBasicMarkdown(value.content);
|
|
42
42
|
if (value.title) {
|
|
43
43
|
const level = value.isH3 ? "###" : "##";
|
|
44
44
|
parts.push(`${level} ${value.title}\n\n${markdown}`);
|
|
@@ -74,7 +74,7 @@ function convertMDNBody(sections: MDNSection[]): string {
|
|
|
74
74
|
if (value.items) {
|
|
75
75
|
for (const item of value.items) {
|
|
76
76
|
parts.push(`**${item.term}**`);
|
|
77
|
-
const desc = htmlToBasicMarkdown(item.description);
|
|
77
|
+
const desc = await htmlToBasicMarkdown(item.description);
|
|
78
78
|
parts.push(desc);
|
|
79
79
|
}
|
|
80
80
|
}
|
|
@@ -83,9 +83,13 @@ function convertMDNBody(sections: MDNSection[]): string {
|
|
|
83
83
|
case "table":
|
|
84
84
|
if (value.rows && value.rows.length > 0) {
|
|
85
85
|
// Simple markdown table
|
|
86
|
-
const header = value.rows[0].map(cell => htmlToBasicMarkdown(cell)).join(" | ");
|
|
86
|
+
const header = (await Promise.all(value.rows[0].map(cell => htmlToBasicMarkdown(cell)))).join(" | ");
|
|
87
87
|
const separator = value.rows[0].map(() => "---").join(" | ");
|
|
88
|
-
const bodyRows =
|
|
88
|
+
const bodyRows = await Promise.all(
|
|
89
|
+
value.rows
|
|
90
|
+
.slice(1)
|
|
91
|
+
.map(async row => (await Promise.all(row.map(cell => htmlToBasicMarkdown(cell)))).join(" | ")),
|
|
92
|
+
);
|
|
89
93
|
|
|
90
94
|
parts.push(`| ${header} |`);
|
|
91
95
|
parts.push(`| ${separator} |`);
|
|
@@ -144,12 +148,12 @@ export const handleMDN: SpecialHandler = async (url: string, timeout: number, si
|
|
|
144
148
|
parts.push(`# ${doc.title}`);
|
|
145
149
|
|
|
146
150
|
if (doc.summary) {
|
|
147
|
-
const summary = htmlToBasicMarkdown(doc.summary);
|
|
151
|
+
const summary = await htmlToBasicMarkdown(doc.summary);
|
|
148
152
|
parts.push(summary);
|
|
149
153
|
}
|
|
150
154
|
|
|
151
155
|
if (doc.body && doc.body.length > 0) {
|
|
152
|
-
const bodyMarkdown = convertMDNBody(doc.body);
|
|
156
|
+
const bodyMarkdown = await convertMDNBody(doc.body);
|
|
153
157
|
parts.push(bodyMarkdown);
|
|
154
158
|
}
|
|
155
159
|
|
|
@@ -125,7 +125,7 @@ export const handlePubDev: SpecialHandler = async (url: string, timeout: number,
|
|
|
125
125
|
/<div[^>]*class="[^"]*markdown-body[^"]*"[^>]*>([\s\S]*?)<\/div>/i,
|
|
126
126
|
);
|
|
127
127
|
if (readmeMatch) {
|
|
128
|
-
const readme = htmlToBasicMarkdown(readmeMatch[1]);
|
|
128
|
+
const readme = await htmlToBasicMarkdown(readmeMatch[1]);
|
|
129
129
|
|
|
130
130
|
if (readme.length > 100) {
|
|
131
131
|
md += `## README\n\n${readme}\n`;
|
package/src/web/scrapers/rawg.ts
CHANGED
|
@@ -63,7 +63,7 @@ export const handleRawg: SpecialHandler = async (
|
|
|
63
63
|
md += `**RAWG:** https://rawg.io/games/${encodeURIComponent(slug)}\n`;
|
|
64
64
|
md += "\n";
|
|
65
65
|
|
|
66
|
-
const description = extractDescription(game);
|
|
66
|
+
const description = await extractDescription(game);
|
|
67
67
|
if (description) {
|
|
68
68
|
md += `## Description\n\n${description}\n`;
|
|
69
69
|
}
|
|
@@ -91,11 +91,11 @@ function requiresApiKey(game: RawgGameResponse): boolean {
|
|
|
91
91
|
return detail.includes("api key") || detail.includes("key is required") || detail.includes("apikey");
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
function extractDescription(game: RawgGameResponse): string | null {
|
|
94
|
+
async function extractDescription(game: RawgGameResponse): Promise<string | null> {
|
|
95
95
|
if (game.description_raw) return game.description_raw.trim();
|
|
96
96
|
if (!game.description) return null;
|
|
97
97
|
|
|
98
|
-
const markdown = htmlToBasicMarkdown(game.description).trim();
|
|
98
|
+
const markdown = (await htmlToBasicMarkdown(game.description)).trim();
|
|
99
99
|
return markdown || null;
|
|
100
100
|
}
|
|
101
101
|
|
|
@@ -101,7 +101,7 @@ export const handleReadTheDocs: SpecialHandler = async (
|
|
|
101
101
|
// If no raw source, convert HTML to markdown
|
|
102
102
|
if (!content && mainContent) {
|
|
103
103
|
const html = mainContent.innerHTML;
|
|
104
|
-
content = htmlToBasicMarkdown(html);
|
|
104
|
+
content = await htmlToBasicMarkdown(html);
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
if (!content) {
|
package/src/web/scrapers/spdx.ts
CHANGED
|
@@ -94,7 +94,7 @@ export const handleSpdx: SpecialHandler = async (
|
|
|
94
94
|
const licenseText = license.licenseText
|
|
95
95
|
? license.licenseText
|
|
96
96
|
: license.licenseTextHtml
|
|
97
|
-
? htmlToBasicMarkdown(license.licenseTextHtml)
|
|
97
|
+
? await htmlToBasicMarkdown(license.licenseTextHtml)
|
|
98
98
|
: null;
|
|
99
99
|
|
|
100
100
|
if (licenseText) {
|
|
@@ -90,7 +90,7 @@ export const handleStackOverflow: SpecialHandler = async (
|
|
|
90
90
|
md += question.is_answered ? " (Answered)" : "";
|
|
91
91
|
md += `\n**Tags:** ${question.tags.join(", ")}\n`;
|
|
92
92
|
md += `**Asked by:** ${question.owner.display_name} · ${formatIsoDate(question.creation_date * 1000)}\n\n`;
|
|
93
|
-
md += `---\n\n## Question\n\n${htmlToBasicMarkdown(question.body)}\n\n`;
|
|
93
|
+
md += `---\n\n## Question\n\n${await htmlToBasicMarkdown(question.body)}\n\n`;
|
|
94
94
|
|
|
95
95
|
// Fetch answers
|
|
96
96
|
const aUrl = `https://api.stackexchange.com/2.3/questions/${questionId}/answers?order=desc&sort=votes&site=${site}&filter=withbody`;
|
|
@@ -103,7 +103,7 @@ export const handleStackOverflow: SpecialHandler = async (
|
|
|
103
103
|
for (const answer of aData.items.slice(0, 5)) {
|
|
104
104
|
const accepted = answer.is_accepted ? " (Accepted)" : "";
|
|
105
105
|
md += `### Score: ${answer.score}${accepted} · by ${answer.owner.display_name}\n\n`;
|
|
106
|
-
md += `${htmlToBasicMarkdown(answer.body)}\n\n---\n\n`;
|
|
106
|
+
md += `${await htmlToBasicMarkdown(answer.body)}\n\n---\n\n`;
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
109
|
}
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* Shared types and utilities for web-fetch handlers
|
|
3
3
|
*/
|
|
4
4
|
import { ptree } from "@oh-my-pi/pi-utils";
|
|
5
|
-
import TurndownService from "turndown";
|
|
6
|
-
|
|
5
|
+
import type TurndownService from "turndown";
|
|
6
|
+
|
|
7
7
|
import { ToolAbortError } from "../../tools/tool-errors";
|
|
8
8
|
|
|
9
9
|
export { formatNumber } from "@oh-my-pi/pi-utils";
|
|
@@ -155,28 +155,8 @@ export async function loadPage(url: string, options: LoadPageOptions = {}): Prom
|
|
|
155
155
|
return { content: "", contentType: "", finalUrl: url, ok: false };
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
-
/** Module-level Turndown instance —
|
|
159
|
-
|
|
160
|
-
headingStyle: "atx",
|
|
161
|
-
codeBlockStyle: "fenced",
|
|
162
|
-
bulletListMarker: "-",
|
|
163
|
-
});
|
|
164
|
-
turndown.use(gfm);
|
|
165
|
-
turndown.addRule("strikethrough", {
|
|
166
|
-
filter: ["del", "s", "strike"],
|
|
167
|
-
replacement(content) {
|
|
168
|
-
return `~~${content}~~`;
|
|
169
|
-
},
|
|
170
|
-
});
|
|
171
|
-
turndown.addRule("heading", {
|
|
172
|
-
filter: ["h1", "h2", "h3", "h4", "h5", "h6"],
|
|
173
|
-
replacement(content, node) {
|
|
174
|
-
const level = Number(node.nodeName.charAt(1));
|
|
175
|
-
const prefix = "#".repeat(level);
|
|
176
|
-
const cleaned = content.replace(/\\([.])/g, "$1").trim();
|
|
177
|
-
return `\n\n${prefix} ${cleaned}\n\n`;
|
|
178
|
-
},
|
|
179
|
-
});
|
|
158
|
+
/** Module-level Turndown instance — built lazily on first use. */
|
|
159
|
+
let turndownPromise: Promise<TurndownService> | undefined;
|
|
180
160
|
|
|
181
161
|
type TurndownListParent = {
|
|
182
162
|
nodeName: string;
|
|
@@ -184,27 +164,61 @@ type TurndownListParent = {
|
|
|
184
164
|
children: ArrayLike<unknown>;
|
|
185
165
|
};
|
|
186
166
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
167
|
+
function getTurndown(): Promise<TurndownService> {
|
|
168
|
+
turndownPromise ||= initTurndown();
|
|
169
|
+
return turndownPromise;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function initTurndown(): Promise<TurndownService> {
|
|
173
|
+
const [{ default: TurndownService }, { gfm }] = await Promise.all([
|
|
174
|
+
import("turndown"),
|
|
175
|
+
import("turndown-plugin-gfm"),
|
|
176
|
+
]);
|
|
177
|
+
const turndown = new TurndownService({
|
|
178
|
+
headingStyle: "atx",
|
|
179
|
+
codeBlockStyle: "fenced",
|
|
180
|
+
bulletListMarker: "-",
|
|
181
|
+
});
|
|
182
|
+
turndown.use(gfm);
|
|
183
|
+
turndown.addRule("strikethrough", {
|
|
184
|
+
filter: ["del", "s", "strike"],
|
|
185
|
+
replacement(content) {
|
|
186
|
+
return `~~${content}~~`;
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
turndown.addRule("heading", {
|
|
190
|
+
filter: ["h1", "h2", "h3", "h4", "h5", "h6"],
|
|
191
|
+
replacement(content, node) {
|
|
192
|
+
const level = Number(node.nodeName.charAt(1));
|
|
193
|
+
const prefix = "#".repeat(level);
|
|
194
|
+
const cleaned = content.replace(/\\([.])/g, "$1").trim();
|
|
195
|
+
return `\n\n${prefix} ${cleaned}\n\n`;
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
turndown.addRule("listItem", {
|
|
199
|
+
filter: "li",
|
|
200
|
+
replacement(content, node, options) {
|
|
201
|
+
content = content.replace(/^\n+/, "").replace(/\n+$/, "\n").replace(/\n/gm, "\n ");
|
|
202
|
+
const parent = node.parentNode as unknown as TurndownListParent | null;
|
|
203
|
+
let prefix = `${options.bulletListMarker} `;
|
|
204
|
+
if (parent?.nodeName === "OL") {
|
|
205
|
+
const start = parent.getAttribute("start");
|
|
206
|
+
const index = Array.prototype.indexOf.call(parent.children, node);
|
|
207
|
+
prefix = `${(start ? Number(start) : 1) + index}. `;
|
|
208
|
+
}
|
|
209
|
+
return prefix + content + (node.nextSibling ? "\n" : "");
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
return turndown;
|
|
213
|
+
}
|
|
201
214
|
|
|
202
215
|
/**
|
|
203
216
|
* Convert HTML to markdown using Turndown with GFM support.
|
|
204
217
|
* Strips script/style tags before conversion.
|
|
205
218
|
*/
|
|
206
|
-
export function htmlToBasicMarkdown(html: string): string {
|
|
219
|
+
export async function htmlToBasicMarkdown(html: string): Promise<string> {
|
|
207
220
|
const cleaned = html.replace(/<script[\s\S]*?<\/script>/gi, "").replace(/<style[\s\S]*?<\/style>/gi, "");
|
|
221
|
+
const turndown = await getTurndown();
|
|
208
222
|
return turndown.turndown(cleaned).trim();
|
|
209
223
|
}
|
|
210
224
|
|
package/src/web/scrapers/w3c.ts
CHANGED
|
@@ -100,7 +100,7 @@ export const handleW3c: SpecialHandler = async (
|
|
|
100
100
|
const title = getString(specPayload, "title");
|
|
101
101
|
const shortnameValue = getString(specPayload, "shortname") ?? shortname;
|
|
102
102
|
const description = getString(specPayload, "description") ?? getString(specPayload, "abstract");
|
|
103
|
-
const abstract = description ? htmlToBasicMarkdown(description) : undefined;
|
|
103
|
+
const abstract = description ? await htmlToBasicMarkdown(description) : undefined;
|
|
104
104
|
|
|
105
105
|
const latestVersionUrl =
|
|
106
106
|
getString(latestPayload, "uri") ??
|
package/src/web/search/index.ts
CHANGED
|
@@ -15,7 +15,7 @@ import webSearchSystemPrompt from "../../prompts/system/web-search.md" with { ty
|
|
|
15
15
|
import webSearchDescription from "../../prompts/tools/web-search.md" with { type: "text" };
|
|
16
16
|
import type { ToolSession } from "../../tools";
|
|
17
17
|
import { formatAge } from "../../tools/render-utils";
|
|
18
|
-
import { getSearchProvider, resolveProviderChain, type SearchProvider } from "./provider";
|
|
18
|
+
import { getSearchProvider, getSearchProviderLabel, resolveProviderChain, type SearchProvider } from "./provider";
|
|
19
19
|
import { renderSearchCall, renderSearchResult, type SearchRenderDetails } from "./render";
|
|
20
20
|
import type { SearchProviderId, SearchResponse } from "./types";
|
|
21
21
|
import { SearchProviderError } from "./types";
|
|
@@ -63,7 +63,7 @@ function formatProviderError(error: unknown, provider: SearchProvider): string {
|
|
|
63
63
|
if (error.provider === "zai") {
|
|
64
64
|
return error.message;
|
|
65
65
|
}
|
|
66
|
-
return `${
|
|
66
|
+
return `${getSearchProviderLabel(error.provider)} authorization failed (${error.status}). Check API key or base URL.`;
|
|
67
67
|
}
|
|
68
68
|
return error.message;
|
|
69
69
|
}
|
|
@@ -139,9 +139,9 @@ async function executeSearch(
|
|
|
139
139
|
): Promise<{ content: Array<{ type: "text"; text: string }>; details: SearchRenderDetails }> {
|
|
140
140
|
const providers =
|
|
141
141
|
params.provider && params.provider !== "auto"
|
|
142
|
-
?
|
|
143
|
-
|
|
144
|
-
|
|
142
|
+
? await getSearchProvider(params.provider).then(provider =>
|
|
143
|
+
provider.isAvailable() ? [provider] : resolveProviderChain("auto"),
|
|
144
|
+
)
|
|
145
145
|
: await resolveProviderChain();
|
|
146
146
|
if (providers.length === 0) {
|
|
147
147
|
const message = "No web search provider configured.";
|
|
@@ -1,39 +1,121 @@
|
|
|
1
|
-
|
|
1
|
+
// Lazy registry of web search providers.
|
|
2
|
+
//
|
|
3
|
+
// Each provider is loaded on first use; importing this module loads zero
|
|
4
|
+
// provider implementations. Provider modules are heavy (each pulls in
|
|
5
|
+
// fetch/parse/format helpers) and only one — at most — is needed per session,
|
|
6
|
+
// so eager construction was wasted work at startup.
|
|
7
|
+
//
|
|
8
|
+
// The `label`/`id` metadata is kept inline so callers needing a display name
|
|
9
|
+
// (error formatting, UI listings) do not force a load.
|
|
10
|
+
|
|
2
11
|
import type { SearchProvider } from "./providers/base";
|
|
3
|
-
import { BraveProvider } from "./providers/brave";
|
|
4
|
-
import { CodexProvider } from "./providers/codex";
|
|
5
|
-
import { ExaProvider } from "./providers/exa";
|
|
6
|
-
import { GeminiProvider } from "./providers/gemini";
|
|
7
|
-
import { JinaProvider } from "./providers/jina";
|
|
8
|
-
import { KagiProvider } from "./providers/kagi";
|
|
9
|
-
import { KimiProvider } from "./providers/kimi";
|
|
10
|
-
import { ParallelProvider } from "./providers/parallel";
|
|
11
|
-
import { PerplexityProvider } from "./providers/perplexity";
|
|
12
|
-
import { SearXNGProvider } from "./providers/searxng";
|
|
13
|
-
import { SyntheticProvider } from "./providers/synthetic";
|
|
14
|
-
import { TavilyProvider } from "./providers/tavily";
|
|
15
|
-
import { ZaiProvider } from "./providers/zai";
|
|
16
12
|
import type { SearchProviderId } from "./types";
|
|
17
13
|
|
|
18
14
|
export type { SearchParams } from "./providers/base";
|
|
19
15
|
export { SearchProvider } from "./providers/base";
|
|
20
16
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
17
|
+
interface ProviderMeta {
|
|
18
|
+
id: SearchProviderId;
|
|
19
|
+
label: string;
|
|
20
|
+
load: () => Promise<SearchProvider>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Lazy factories. Each `load()` dynamic-imports its provider module on first call. */
|
|
24
|
+
const PROVIDER_META: Record<SearchProviderId, ProviderMeta> = {
|
|
25
|
+
exa: {
|
|
26
|
+
id: "exa",
|
|
27
|
+
label: "Exa",
|
|
28
|
+
load: async () => new (await import("./providers/exa")).ExaProvider(),
|
|
29
|
+
},
|
|
30
|
+
brave: {
|
|
31
|
+
id: "brave",
|
|
32
|
+
label: "Brave",
|
|
33
|
+
load: async () => new (await import("./providers/brave")).BraveProvider(),
|
|
34
|
+
},
|
|
35
|
+
jina: {
|
|
36
|
+
id: "jina",
|
|
37
|
+
label: "Jina",
|
|
38
|
+
load: async () => new (await import("./providers/jina")).JinaProvider(),
|
|
39
|
+
},
|
|
40
|
+
perplexity: {
|
|
41
|
+
id: "perplexity",
|
|
42
|
+
label: "Perplexity",
|
|
43
|
+
load: async () => new (await import("./providers/perplexity")).PerplexityProvider(),
|
|
44
|
+
},
|
|
45
|
+
kimi: {
|
|
46
|
+
id: "kimi",
|
|
47
|
+
label: "Kimi",
|
|
48
|
+
load: async () => new (await import("./providers/kimi")).KimiProvider(),
|
|
49
|
+
},
|
|
50
|
+
zai: {
|
|
51
|
+
id: "zai",
|
|
52
|
+
label: "Z.AI",
|
|
53
|
+
load: async () => new (await import("./providers/zai")).ZaiProvider(),
|
|
54
|
+
},
|
|
55
|
+
anthropic: {
|
|
56
|
+
id: "anthropic",
|
|
57
|
+
label: "Anthropic",
|
|
58
|
+
load: async () => new (await import("./providers/anthropic")).AnthropicProvider(),
|
|
59
|
+
},
|
|
60
|
+
gemini: {
|
|
61
|
+
id: "gemini",
|
|
62
|
+
label: "Gemini",
|
|
63
|
+
load: async () => new (await import("./providers/gemini")).GeminiProvider(),
|
|
64
|
+
},
|
|
65
|
+
codex: {
|
|
66
|
+
id: "codex",
|
|
67
|
+
label: "Codex",
|
|
68
|
+
load: async () => new (await import("./providers/codex")).CodexProvider(),
|
|
69
|
+
},
|
|
70
|
+
tavily: {
|
|
71
|
+
id: "tavily",
|
|
72
|
+
label: "Tavily",
|
|
73
|
+
load: async () => new (await import("./providers/tavily")).TavilyProvider(),
|
|
74
|
+
},
|
|
75
|
+
parallel: {
|
|
76
|
+
id: "parallel",
|
|
77
|
+
label: "Parallel",
|
|
78
|
+
load: async () => new (await import("./providers/parallel")).ParallelProvider(),
|
|
79
|
+
},
|
|
80
|
+
kagi: {
|
|
81
|
+
id: "kagi",
|
|
82
|
+
label: "Kagi",
|
|
83
|
+
load: async () => new (await import("./providers/kagi")).KagiProvider(),
|
|
84
|
+
},
|
|
85
|
+
synthetic: {
|
|
86
|
+
id: "synthetic",
|
|
87
|
+
label: "Synthetic",
|
|
88
|
+
load: async () => new (await import("./providers/synthetic")).SyntheticProvider(),
|
|
89
|
+
},
|
|
90
|
+
searxng: {
|
|
91
|
+
id: "searxng",
|
|
92
|
+
label: "SearXNG",
|
|
93
|
+
load: async () => new (await import("./providers/searxng")).SearXNGProvider(),
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const instanceCache = new Map<SearchProviderId, SearchProvider>();
|
|
98
|
+
|
|
99
|
+
/** Cheap, sync metadata accessor — never triggers a provider load. */
|
|
100
|
+
export function getSearchProviderLabel(id: SearchProviderId): string {
|
|
101
|
+
return PROVIDER_META[id]?.label ?? id;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Resolve and cache a provider instance. First call for a given id loads the
|
|
106
|
+
* underlying module; subsequent calls return the cached singleton.
|
|
107
|
+
*/
|
|
108
|
+
export async function getSearchProvider(id: SearchProviderId): Promise<SearchProvider> {
|
|
109
|
+
const cached = instanceCache.get(id);
|
|
110
|
+
if (cached) return cached;
|
|
111
|
+
const meta = PROVIDER_META[id];
|
|
112
|
+
if (!meta) {
|
|
113
|
+
throw new Error(`Unknown search provider: ${id}`);
|
|
114
|
+
}
|
|
115
|
+
const provider = await meta.load();
|
|
116
|
+
instanceCache.set(id, provider);
|
|
117
|
+
return provider;
|
|
118
|
+
}
|
|
37
119
|
|
|
38
120
|
export const SEARCH_PROVIDER_ORDER: SearchProviderId[] = [
|
|
39
121
|
"tavily",
|
|
@@ -52,10 +134,6 @@ export const SEARCH_PROVIDER_ORDER: SearchProviderId[] = [
|
|
|
52
134
|
"searxng",
|
|
53
135
|
];
|
|
54
136
|
|
|
55
|
-
export function getSearchProvider(provider: SearchProviderId): SearchProvider {
|
|
56
|
-
return SEARCH_PROVIDERS[provider];
|
|
57
|
-
}
|
|
58
|
-
|
|
59
137
|
/** Preferred provider set via settings (default: auto) */
|
|
60
138
|
let preferredProvId: SearchProviderId | "auto" = "auto";
|
|
61
139
|
|
|
@@ -64,22 +142,26 @@ export function setPreferredSearchProvider(provider: SearchProviderId | "auto"):
|
|
|
64
142
|
preferredProvId = provider;
|
|
65
143
|
}
|
|
66
144
|
|
|
67
|
-
/**
|
|
145
|
+
/**
|
|
146
|
+
* Determine which providers are configured and currently available.
|
|
147
|
+
* Each candidate is loaded (and its `isAvailable()` called) only as the chain
|
|
148
|
+
* is walked, so unconfigured providers never pay the load cost.
|
|
149
|
+
*/
|
|
68
150
|
export async function resolveProviderChain(
|
|
69
151
|
preferredProvider: SearchProviderId | "auto" = preferredProvId,
|
|
70
152
|
): Promise<SearchProvider[]> {
|
|
71
153
|
const providers: SearchProvider[] = [];
|
|
72
154
|
|
|
73
155
|
if (preferredProvider !== "auto") {
|
|
74
|
-
|
|
75
|
-
|
|
156
|
+
const provider = await getSearchProvider(preferredProvider);
|
|
157
|
+
if (await provider.isAvailable()) {
|
|
158
|
+
providers.push(provider);
|
|
76
159
|
}
|
|
77
160
|
}
|
|
78
161
|
|
|
79
162
|
for (const id of SEARCH_PROVIDER_ORDER) {
|
|
80
163
|
if (id === preferredProvider) continue;
|
|
81
|
-
|
|
82
|
-
const provider = getSearchProvider(id);
|
|
164
|
+
const provider = await getSearchProvider(id);
|
|
83
165
|
if (await provider.isAvailable()) {
|
|
84
166
|
providers.push(provider);
|
|
85
167
|
}
|