@herbertgao/pi-extensions 2026.10.0 → 2026.10.1
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 +7 -7
- package/THIRD_PARTY_NOTICES.md +2 -2
- package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-audit.md +1 -1
- package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-debt.md +1 -1
- package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-gain.md +1 -1
- package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-review.md +1 -1
- package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail.md +1 -1
- package/node_modules/@dietrichgebert/ponytail/.opencode/plugins/index.js +4 -0
- package/node_modules/@dietrichgebert/ponytail/.opencode/plugins/ponytail-frontmatter.cjs +43 -3
- package/node_modules/@dietrichgebert/ponytail/.opencode/plugins/ponytail.mjs +142 -59
- package/node_modules/@dietrichgebert/ponytail/.qoder-plugin/plugin.json +1 -1
- package/node_modules/@dietrichgebert/ponytail/README.es.md +87 -204
- package/node_modules/@dietrichgebert/ponytail/README.ja.md +212 -0
- package/node_modules/@dietrichgebert/ponytail/README.ko.md +92 -226
- package/node_modules/@dietrichgebert/ponytail/README.md +58 -244
- package/node_modules/@dietrichgebert/ponytail/README.zh-CN.md +212 -0
- package/node_modules/@dietrichgebert/ponytail/hooks/claude-codex-hooks.json +3 -3
- package/node_modules/@dietrichgebert/ponytail/hooks/copilot-hooks.json +4 -4
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-activate.js +63 -16
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-config.js +8 -7
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-mode-tracker.js +46 -23
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-runtime.js +39 -7
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-subagent.js +20 -3
- package/node_modules/@dietrichgebert/ponytail/hooks/qoder-hooks.json +3 -3
- package/node_modules/@dietrichgebert/ponytail/package.json +2 -2
- package/node_modules/@dietrichgebert/ponytail/pi-extension/index.js +47 -17
- package/node_modules/@dietrichgebert/ponytail/scripts/uninstall.js +21 -7
- package/node_modules/@dietrichgebert/ponytail/skills/plugin.json +5 -0
- package/node_modules/@dietrichgebert/ponytail/skills/ponytail-audit/SKILL.md +7 -2
- package/node_modules/@dietrichgebert/ponytail/skills/ponytail-debt/SKILL.md +1 -1
- package/node_modules/@dietrichgebert/ponytail/skills/ponytail-gain/SKILL.md +16 -14
- package/node_modules/@dietrichgebert/ponytail/skills/ponytail-help/SKILL.md +3 -3
- package/node_modules/@dietrichgebert/ponytail/skills/ponytail-review/SKILL.md +12 -7
- package/node_modules/@narumitw/pi-caffeinate/package.json +4 -4
- package/node_modules/pi-typesafe/README.md +1 -1
- package/node_modules/pi-typesafe/dist/backends.d.ts +15 -5
- package/node_modules/pi-typesafe/dist/backends.js +18 -0
- package/node_modules/pi-typesafe/dist/client.d.ts +4 -3
- package/node_modules/pi-typesafe/dist/client.js +22 -7
- package/node_modules/pi-typesafe/dist/index.d.ts +2 -2
- package/node_modules/pi-typesafe/dist/index.js +1 -1
- package/node_modules/pi-typesafe/dist/usage.d.ts +14 -4
- package/node_modules/pi-typesafe/dist/usage.js +147 -42
- package/node_modules/pi-typesafe/package.json +1 -1
- package/node_modules/pi-web-access/CHANGELOG.md +48 -0
- package/node_modules/pi-web-access/README.md +57 -11
- package/node_modules/pi-web-access/brave-rate-limit.ts +8 -2
- package/node_modules/pi-web-access/curator-page.ts +5 -1
- package/node_modules/pi-web-access/curator-server.ts +2 -0
- package/node_modules/pi-web-access/declared-web-links.ts +2 -2
- package/node_modules/pi-web-access/degoog.ts +323 -0
- package/node_modules/pi-web-access/dist/index.js +3483 -2745
- package/node_modules/pi-web-access/dist/mcp-cli.js +27443 -0
- package/node_modules/pi-web-access/exa.ts +5 -1
- package/node_modules/pi-web-access/extract.ts +41 -3
- package/node_modules/pi-web-access/firecrawl.ts +35 -4
- package/node_modules/pi-web-access/gemini-search.ts +34 -11
- package/node_modules/pi-web-access/gemini-web.ts +14 -9
- package/node_modules/pi-web-access/index.ts +153 -1202
- package/node_modules/pi-web-access/keenable.ts +221 -0
- package/node_modules/pi-web-access/mcp-cli.ts +40 -0
- package/node_modules/pi-web-access/mcp-server.ts +81 -0
- package/node_modules/pi-web-access/mcp-tool-definitions.ts +117 -0
- package/node_modules/pi-web-access/openai-search.ts +34 -1
- package/node_modules/pi-web-access/package.json +23 -5
- package/node_modules/pi-web-access/perplexity.ts +64 -56
- package/node_modules/pi-web-access/tool-arguments.ts +20 -0
- package/node_modules/pi-web-access/video-extract.ts +1 -1
- package/node_modules/pi-web-access/web-tool-contract.ts +86 -0
- package/node_modules/pi-web-access/web-tool-core.ts +1502 -0
- package/node_modules/pi-web-access/youtube-extract.ts +2 -5
- package/package.json +6 -6
- package/node_modules/@dietrichgebert/ponytail/assets/benchmark-3model.svg +0 -21
- package/node_modules/@dietrichgebert/ponytail/assets/benchmark-agentic.svg +0 -62
- package/node_modules/@dietrichgebert/ponytail/assets/logo-dark.png +0 -0
- package/node_modules/@dietrichgebert/ponytail/assets/logo-dark.svg +0 -115
- package/node_modules/@dietrichgebert/ponytail/assets/logo-greenpt-dark.svg +0 -27
- package/node_modules/@dietrichgebert/ponytail/assets/logo-greenpt.svg +0 -27
- package/node_modules/@dietrichgebert/ponytail/assets/logo.png +0 -0
- package/node_modules/@dietrichgebert/ponytail/assets/retriever-icon.png +0 -0
- package/node_modules/@dietrichgebert/ponytail/assets/retriever-logo-dark.svg +0 -4
- package/node_modules/@dietrichgebert/ponytail/assets/retriever-logo-light.svg +0 -4
- package/node_modules/@dietrichgebert/ponytail/assets/social-preview.png +0 -0
- package/node_modules/@dietrichgebert/ponytail/assets/waitlist-banner-es.png +0 -0
- package/node_modules/@dietrichgebert/ponytail/assets/waitlist-banner-ko.png +0 -0
- package/node_modules/@dietrichgebert/ponytail/assets/waitlist-banner.png +0 -0
|
@@ -2,9 +2,11 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
2
2
|
import { activityMonitor } from "./activity.ts";
|
|
3
3
|
import type { ExtractedContent } from "./extract.ts";
|
|
4
4
|
import { hasCredentialSource, redactCredential, resolveCredential } from "./credential-source.ts";
|
|
5
|
+
import { formatSearchResultsAsAnswer } from "./search-answer-formatting.ts";
|
|
5
6
|
import { getWebSearchConfigPath } from "./utils.ts";
|
|
6
7
|
|
|
7
|
-
const
|
|
8
|
+
const PERPLEXITY_SEARCH_URL = "https://api.perplexity.ai/search";
|
|
9
|
+
const PERPLEXITY_CHAT_URL = "https://api.perplexity.ai/chat/completions";
|
|
8
10
|
const CONFIG_PATH = getWebSearchConfigPath();
|
|
9
11
|
|
|
10
12
|
const RATE_LIMIT = {
|
|
@@ -105,22 +107,19 @@ export function isPerplexityAvailable(): boolean {
|
|
|
105
107
|
});
|
|
106
108
|
}
|
|
107
109
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
// Preserve citation numbering by keeping the prefix through the highest cited index, capped at 20.
|
|
112
|
-
function citationsToKeep(answer: string, available: number, numResults: number): number {
|
|
113
|
-
let highestCited = 0;
|
|
114
|
-
for (const match of answer.matchAll(/\[(\d{1,3})\]/g)) {
|
|
115
|
-
highestCited = Math.max(highestCited, Number(match[1]));
|
|
116
|
-
}
|
|
117
|
-
return Math.min(available, MAX_CITATIONS, Math.max(numResults, highestCited));
|
|
110
|
+
interface PerplexityRequestOptions {
|
|
111
|
+
activityQuery: string;
|
|
112
|
+
signal?: AbortSignal;
|
|
118
113
|
}
|
|
119
114
|
|
|
120
|
-
|
|
115
|
+
async function postPerplexity(
|
|
116
|
+
url: string,
|
|
117
|
+
body: Record<string, unknown>,
|
|
118
|
+
options: PerplexityRequestOptions,
|
|
119
|
+
): Promise<Record<string, unknown>> {
|
|
121
120
|
checkRateLimit();
|
|
122
121
|
|
|
123
|
-
const activityId = activityMonitor.logStart({ type: "api", query });
|
|
122
|
+
const activityId = activityMonitor.logStart({ type: "api", query: options.activityQuery });
|
|
124
123
|
|
|
125
124
|
activityMonitor.updateRateLimit({
|
|
126
125
|
used: requestTimestamps.length,
|
|
@@ -130,37 +129,16 @@ export async function searchWithPerplexity(query: string, options: SearchOptions
|
|
|
130
129
|
});
|
|
131
130
|
|
|
132
131
|
const apiKey = await getApiKey(options.signal);
|
|
133
|
-
const numResults = typeof options.numResults === "number" && Number.isFinite(options.numResults)
|
|
134
|
-
? Math.max(1, Math.min(Math.floor(options.numResults), 20))
|
|
135
|
-
: 5;
|
|
136
|
-
|
|
137
|
-
const requestBody: Record<string, unknown> = {
|
|
138
|
-
model: "sonar",
|
|
139
|
-
messages: [{ role: "user", content: query }],
|
|
140
|
-
max_tokens: 1024,
|
|
141
|
-
return_related_questions: false,
|
|
142
|
-
};
|
|
143
|
-
|
|
144
|
-
if (options.recencyFilter) {
|
|
145
|
-
requestBody.search_recency_filter = options.recencyFilter;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
if (options.domainFilter && options.domainFilter.length > 0) {
|
|
149
|
-
const validated = validateDomainFilter(options.domainFilter);
|
|
150
|
-
if (validated.length > 0) {
|
|
151
|
-
requestBody.search_domain_filter = validated;
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
132
|
|
|
155
133
|
let response: Response;
|
|
156
134
|
try {
|
|
157
|
-
response = await fetch(
|
|
135
|
+
response = await fetch(url, {
|
|
158
136
|
method: "POST",
|
|
159
137
|
headers: {
|
|
160
138
|
Authorization: `Bearer ${apiKey}`,
|
|
161
139
|
"Content-Type": "application/json",
|
|
162
140
|
},
|
|
163
|
-
body: JSON.stringify(
|
|
141
|
+
body: JSON.stringify(body),
|
|
164
142
|
...(options.signal ? { signal: options.signal } : {}),
|
|
165
143
|
});
|
|
166
144
|
} catch (err) {
|
|
@@ -177,39 +155,69 @@ export async function searchWithPerplexity(query: string, options: SearchOptions
|
|
|
177
155
|
throw redactedError;
|
|
178
156
|
}
|
|
179
157
|
|
|
158
|
+
activityMonitor.logComplete(activityId, response.status);
|
|
159
|
+
|
|
180
160
|
if (!response.ok) {
|
|
181
|
-
activityMonitor.logComplete(activityId, response.status);
|
|
182
161
|
const errorText = redactCredential(await response.text(), apiKey);
|
|
183
162
|
throw new Error(`Perplexity API error ${response.status}: ${errorText}`);
|
|
184
163
|
}
|
|
185
164
|
|
|
186
|
-
let data: Record<string, unknown>;
|
|
187
165
|
try {
|
|
188
|
-
|
|
166
|
+
return await response.json();
|
|
189
167
|
} catch (err) {
|
|
190
|
-
activityMonitor.logComplete(activityId, response.status);
|
|
191
168
|
const message = err instanceof Error ? err.message : String(err);
|
|
192
169
|
throw new Error(`Perplexity API returned invalid JSON: ${message}`);
|
|
193
170
|
}
|
|
171
|
+
}
|
|
194
172
|
|
|
195
|
-
|
|
196
|
-
const
|
|
173
|
+
export async function searchWithPerplexity(query: string, options: SearchOptions = {}): Promise<SearchResponse> {
|
|
174
|
+
const numResults = typeof options.numResults === "number" && Number.isFinite(options.numResults)
|
|
175
|
+
? Math.max(1, Math.min(Math.floor(options.numResults), 20))
|
|
176
|
+
: 5;
|
|
197
177
|
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
178
|
+
const requestBody: Record<string, unknown> = {
|
|
179
|
+
query,
|
|
180
|
+
max_results: numResults,
|
|
181
|
+
search_type: "fast",
|
|
182
|
+
search_context_size: "medium",
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
if (options.recencyFilter) {
|
|
186
|
+
requestBody.search_recency_filter = options.recencyFilter;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (options.domainFilter && options.domainFilter.length > 0) {
|
|
190
|
+
const validated = validateDomainFilter(options.domainFilter);
|
|
191
|
+
if (validated.length > 0) {
|
|
192
|
+
requestBody.search_domain_filter = validated;
|
|
210
193
|
}
|
|
211
194
|
}
|
|
212
195
|
|
|
213
|
-
|
|
214
|
-
|
|
196
|
+
const data = await postPerplexity(PERPLEXITY_SEARCH_URL, requestBody, { activityQuery: query, signal: options.signal });
|
|
197
|
+
|
|
198
|
+
const results: SearchResult[] = [];
|
|
199
|
+
for (const entry of Array.isArray(data.results) ? data.results : []) {
|
|
200
|
+
if (!entry || typeof entry !== "object" || typeof entry.url !== "string" || !entry.url) continue;
|
|
201
|
+
results.push({
|
|
202
|
+
title: typeof entry.title === "string" && entry.title.trim() ? entry.title.trim() : `Source ${results.length + 1}`,
|
|
203
|
+
url: entry.url,
|
|
204
|
+
snippet: typeof entry.snippet === "string" ? entry.snippet.trim() : "",
|
|
205
|
+
});
|
|
206
|
+
if (results.length >= numResults) break;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return { answer: formatSearchResultsAsAnswer(results), results };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** Asks Sonar for a prose answer. The Search API returns only ranked pages, so prompts that need prose use this. */
|
|
213
|
+
export async function askPerplexity(prompt: string, options: { signal?: AbortSignal } = {}): Promise<string> {
|
|
214
|
+
const data = await postPerplexity(PERPLEXITY_CHAT_URL, {
|
|
215
|
+
model: "sonar",
|
|
216
|
+
messages: [{ role: "user", content: prompt }],
|
|
217
|
+
max_tokens: 1024,
|
|
218
|
+
return_related_questions: false,
|
|
219
|
+
}, { activityQuery: prompt, signal: options.signal });
|
|
220
|
+
|
|
221
|
+
const content = (data.choices as Array<{ message?: { content?: unknown } }> | undefined)?.[0]?.message?.content;
|
|
222
|
+
return typeof content === "string" ? content : "";
|
|
215
223
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Some models send array arguments as a JSON string, for example
|
|
2
|
+
// `provider: "[\"parallel-mcp\"]"`, which fails schema validation before the tool
|
|
3
|
+
// runs. Parse such strings back into arrays of strings; leave anything else as is.
|
|
4
|
+
export function parseStringifiedArrays(args: unknown, keys: readonly string[]): unknown {
|
|
5
|
+
if (!args || typeof args !== "object" || Array.isArray(args)) return args;
|
|
6
|
+
const input = args as Record<string, unknown>;
|
|
7
|
+
let output: Record<string, unknown> | undefined;
|
|
8
|
+
for (const key of keys) {
|
|
9
|
+
const value = input[key];
|
|
10
|
+
if (typeof value !== "string" || !value.trim().startsWith("[")) continue;
|
|
11
|
+
try {
|
|
12
|
+
const parsed: unknown = JSON.parse(value);
|
|
13
|
+
if (Array.isArray(parsed) && parsed.every((item) => typeof item === "string")) {
|
|
14
|
+
output ??= { ...input };
|
|
15
|
+
output[key] = parsed;
|
|
16
|
+
}
|
|
17
|
+
} catch {}
|
|
18
|
+
}
|
|
19
|
+
return output ?? args;
|
|
20
|
+
}
|
|
@@ -249,7 +249,7 @@ async function tryVideoGeminiWeb(
|
|
|
249
249
|
if (signal?.aborted) return null;
|
|
250
250
|
|
|
251
251
|
const text = await queryWithCookies(prompt, cookies, {
|
|
252
|
-
files: [info.absolutePath],
|
|
252
|
+
files: [{ path: info.absolutePath, mimeType: info.mimeType }],
|
|
253
253
|
...(model !== "gemini-3.6-flash" ? { model } : {}),
|
|
254
254
|
signal,
|
|
255
255
|
timeoutMs: 180000,
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// Host-neutral contract for the web tools. The Pi extension (index.ts) and the
|
|
2
|
+
// MCP server (mcp-server.ts) both drive tools through WebToolCore so tool
|
|
3
|
+
// behavior lives in one place; each host keeps its own schemas and transport.
|
|
4
|
+
|
|
5
|
+
import type { FindMode } from "./content-find.ts";
|
|
6
|
+
import type { RecencyFilter } from "./source-check.ts";
|
|
7
|
+
|
|
8
|
+
export type WebToolContent =
|
|
9
|
+
| { type: "text"; text: string }
|
|
10
|
+
| { type: "image"; data: string; mimeType: string };
|
|
11
|
+
|
|
12
|
+
export interface WebToolResult {
|
|
13
|
+
content: WebToolContent[];
|
|
14
|
+
details: Record<string, unknown>;
|
|
15
|
+
isError?: boolean;
|
|
16
|
+
/** Data for programmatic callers such as Pi codemode scripts; never sent to the model or over MCP. */
|
|
17
|
+
structuredContent?: WebSearchStructuredContent | FetchContentStructuredContent;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type WebSearchStructuredContent = {
|
|
21
|
+
responseId: string;
|
|
22
|
+
fetchId: string | null;
|
|
23
|
+
queries: Array<{
|
|
24
|
+
query: string;
|
|
25
|
+
answer: string;
|
|
26
|
+
error: string | null;
|
|
27
|
+
provider?: string;
|
|
28
|
+
providers?: string[];
|
|
29
|
+
results: Array<{ title: string; url: string; snippet: string }>;
|
|
30
|
+
}>;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export type FetchContentStructuredContent = {
|
|
34
|
+
responseId: string | null;
|
|
35
|
+
urls: Array<{ url: string; title: string; content: string; error: string | null; mimeType?: string; status?: number; duration?: number }>;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export interface WebSearchCallParams {
|
|
39
|
+
query?: string;
|
|
40
|
+
queries?: string[];
|
|
41
|
+
numResults?: number;
|
|
42
|
+
includeContent?: boolean;
|
|
43
|
+
recencyFilter?: RecencyFilter;
|
|
44
|
+
domainFilter?: string[];
|
|
45
|
+
category?: string;
|
|
46
|
+
provider?: string | string[];
|
|
47
|
+
proxy?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface FetchContentCallParams {
|
|
51
|
+
url?: string;
|
|
52
|
+
urls?: string[];
|
|
53
|
+
mode?: string;
|
|
54
|
+
forceClone?: boolean;
|
|
55
|
+
proxy?: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface GetSearchContentCallParams {
|
|
59
|
+
responseId: string;
|
|
60
|
+
query?: string;
|
|
61
|
+
queryIndex?: number;
|
|
62
|
+
url?: string;
|
|
63
|
+
urlIndex?: number;
|
|
64
|
+
offset?: number;
|
|
65
|
+
limit?: number;
|
|
66
|
+
findText?: string | string[];
|
|
67
|
+
findMode?: FindMode;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface SourceCheckCallParams {
|
|
71
|
+
claim: string;
|
|
72
|
+
queries?: string[];
|
|
73
|
+
numResults?: number;
|
|
74
|
+
fetchContent?: boolean;
|
|
75
|
+
recencyFilter?: RecencyFilter;
|
|
76
|
+
domainFilter?: string[];
|
|
77
|
+
provider?: string | string[];
|
|
78
|
+
proxy?: string;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface WebToolCore {
|
|
82
|
+
webSearch(params: WebSearchCallParams, signal?: AbortSignal): Promise<WebToolResult>;
|
|
83
|
+
fetchContent(params: FetchContentCallParams, signal?: AbortSignal): Promise<WebToolResult>;
|
|
84
|
+
getSearchContent(params: GetSearchContentCallParams, signal?: AbortSignal): Promise<WebToolResult>;
|
|
85
|
+
sourceCheck(params: SourceCheckCallParams, signal?: AbortSignal): Promise<WebToolResult>;
|
|
86
|
+
}
|