@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.
Files changed (86) hide show
  1. package/README.md +7 -7
  2. package/THIRD_PARTY_NOTICES.md +2 -2
  3. package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-audit.md +1 -1
  4. package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-debt.md +1 -1
  5. package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-gain.md +1 -1
  6. package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-review.md +1 -1
  7. package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail.md +1 -1
  8. package/node_modules/@dietrichgebert/ponytail/.opencode/plugins/index.js +4 -0
  9. package/node_modules/@dietrichgebert/ponytail/.opencode/plugins/ponytail-frontmatter.cjs +43 -3
  10. package/node_modules/@dietrichgebert/ponytail/.opencode/plugins/ponytail.mjs +142 -59
  11. package/node_modules/@dietrichgebert/ponytail/.qoder-plugin/plugin.json +1 -1
  12. package/node_modules/@dietrichgebert/ponytail/README.es.md +87 -204
  13. package/node_modules/@dietrichgebert/ponytail/README.ja.md +212 -0
  14. package/node_modules/@dietrichgebert/ponytail/README.ko.md +92 -226
  15. package/node_modules/@dietrichgebert/ponytail/README.md +58 -244
  16. package/node_modules/@dietrichgebert/ponytail/README.zh-CN.md +212 -0
  17. package/node_modules/@dietrichgebert/ponytail/hooks/claude-codex-hooks.json +3 -3
  18. package/node_modules/@dietrichgebert/ponytail/hooks/copilot-hooks.json +4 -4
  19. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-activate.js +63 -16
  20. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-config.js +8 -7
  21. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-mode-tracker.js +46 -23
  22. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-runtime.js +39 -7
  23. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-subagent.js +20 -3
  24. package/node_modules/@dietrichgebert/ponytail/hooks/qoder-hooks.json +3 -3
  25. package/node_modules/@dietrichgebert/ponytail/package.json +2 -2
  26. package/node_modules/@dietrichgebert/ponytail/pi-extension/index.js +47 -17
  27. package/node_modules/@dietrichgebert/ponytail/scripts/uninstall.js +21 -7
  28. package/node_modules/@dietrichgebert/ponytail/skills/plugin.json +5 -0
  29. package/node_modules/@dietrichgebert/ponytail/skills/ponytail-audit/SKILL.md +7 -2
  30. package/node_modules/@dietrichgebert/ponytail/skills/ponytail-debt/SKILL.md +1 -1
  31. package/node_modules/@dietrichgebert/ponytail/skills/ponytail-gain/SKILL.md +16 -14
  32. package/node_modules/@dietrichgebert/ponytail/skills/ponytail-help/SKILL.md +3 -3
  33. package/node_modules/@dietrichgebert/ponytail/skills/ponytail-review/SKILL.md +12 -7
  34. package/node_modules/@narumitw/pi-caffeinate/package.json +4 -4
  35. package/node_modules/pi-typesafe/README.md +1 -1
  36. package/node_modules/pi-typesafe/dist/backends.d.ts +15 -5
  37. package/node_modules/pi-typesafe/dist/backends.js +18 -0
  38. package/node_modules/pi-typesafe/dist/client.d.ts +4 -3
  39. package/node_modules/pi-typesafe/dist/client.js +22 -7
  40. package/node_modules/pi-typesafe/dist/index.d.ts +2 -2
  41. package/node_modules/pi-typesafe/dist/index.js +1 -1
  42. package/node_modules/pi-typesafe/dist/usage.d.ts +14 -4
  43. package/node_modules/pi-typesafe/dist/usage.js +147 -42
  44. package/node_modules/pi-typesafe/package.json +1 -1
  45. package/node_modules/pi-web-access/CHANGELOG.md +48 -0
  46. package/node_modules/pi-web-access/README.md +57 -11
  47. package/node_modules/pi-web-access/brave-rate-limit.ts +8 -2
  48. package/node_modules/pi-web-access/curator-page.ts +5 -1
  49. package/node_modules/pi-web-access/curator-server.ts +2 -0
  50. package/node_modules/pi-web-access/declared-web-links.ts +2 -2
  51. package/node_modules/pi-web-access/degoog.ts +323 -0
  52. package/node_modules/pi-web-access/dist/index.js +3483 -2745
  53. package/node_modules/pi-web-access/dist/mcp-cli.js +27443 -0
  54. package/node_modules/pi-web-access/exa.ts +5 -1
  55. package/node_modules/pi-web-access/extract.ts +41 -3
  56. package/node_modules/pi-web-access/firecrawl.ts +35 -4
  57. package/node_modules/pi-web-access/gemini-search.ts +34 -11
  58. package/node_modules/pi-web-access/gemini-web.ts +14 -9
  59. package/node_modules/pi-web-access/index.ts +153 -1202
  60. package/node_modules/pi-web-access/keenable.ts +221 -0
  61. package/node_modules/pi-web-access/mcp-cli.ts +40 -0
  62. package/node_modules/pi-web-access/mcp-server.ts +81 -0
  63. package/node_modules/pi-web-access/mcp-tool-definitions.ts +117 -0
  64. package/node_modules/pi-web-access/openai-search.ts +34 -1
  65. package/node_modules/pi-web-access/package.json +23 -5
  66. package/node_modules/pi-web-access/perplexity.ts +64 -56
  67. package/node_modules/pi-web-access/tool-arguments.ts +20 -0
  68. package/node_modules/pi-web-access/video-extract.ts +1 -1
  69. package/node_modules/pi-web-access/web-tool-contract.ts +86 -0
  70. package/node_modules/pi-web-access/web-tool-core.ts +1502 -0
  71. package/node_modules/pi-web-access/youtube-extract.ts +2 -5
  72. package/package.json +6 -6
  73. package/node_modules/@dietrichgebert/ponytail/assets/benchmark-3model.svg +0 -21
  74. package/node_modules/@dietrichgebert/ponytail/assets/benchmark-agentic.svg +0 -62
  75. package/node_modules/@dietrichgebert/ponytail/assets/logo-dark.png +0 -0
  76. package/node_modules/@dietrichgebert/ponytail/assets/logo-dark.svg +0 -115
  77. package/node_modules/@dietrichgebert/ponytail/assets/logo-greenpt-dark.svg +0 -27
  78. package/node_modules/@dietrichgebert/ponytail/assets/logo-greenpt.svg +0 -27
  79. package/node_modules/@dietrichgebert/ponytail/assets/logo.png +0 -0
  80. package/node_modules/@dietrichgebert/ponytail/assets/retriever-icon.png +0 -0
  81. package/node_modules/@dietrichgebert/ponytail/assets/retriever-logo-dark.svg +0 -4
  82. package/node_modules/@dietrichgebert/ponytail/assets/retriever-logo-light.svg +0 -4
  83. package/node_modules/@dietrichgebert/ponytail/assets/social-preview.png +0 -0
  84. package/node_modules/@dietrichgebert/ponytail/assets/waitlist-banner-es.png +0 -0
  85. package/node_modules/@dietrichgebert/ponytail/assets/waitlist-banner-ko.png +0 -0
  86. 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 PERPLEXITY_API_URL = "https://api.perplexity.ai/chat/completions";
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
- /** Hard ceiling on kept citations, matching the `numResults` clamp. */
109
- const MAX_CITATIONS = 20;
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
- export async function searchWithPerplexity(query: string, options: SearchOptions = {}): Promise<SearchResponse> {
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(PERPLEXITY_API_URL, {
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(requestBody),
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
- data = await response.json();
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
- const answer = (data.choices as Array<{ message?: { content?: string } }>)?.[0]?.message?.content || "";
196
- const citations = Array.isArray(data.citations) ? data.citations : [];
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 results: SearchResult[] = [];
199
- const citationCount = citationsToKeep(answer, citations.length, numResults);
200
- for (let i = 0; i < citationCount; i++) {
201
- const citation = citations[i];
202
- if (typeof citation === "string") {
203
- results.push({ title: `Source ${i + 1}`, url: citation, snippet: "" });
204
- } else if (citation && typeof citation === "object" && typeof citation.url === "string") {
205
- results.push({
206
- title: citation.title || `Source ${i + 1}`,
207
- url: citation.url,
208
- snippet: "",
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
- activityMonitor.logComplete(activityId, response.status);
214
- return { answer, results };
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
+ }