@oh-my-pi/pi-coding-agent 16.2.4 → 16.2.6
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 +37 -0
- package/dist/cli.js +3211 -3231
- package/dist/types/cli/bench-cli.d.ts +6 -1
- package/dist/types/commands/bench.d.ts +8 -0
- package/dist/types/config/model-discovery.d.ts +6 -1
- package/dist/types/config/settings-schema.d.ts +1 -1
- package/dist/types/edit/index.d.ts +1 -0
- package/dist/types/edit/renderer.d.ts +4 -0
- package/dist/types/edit/snapshot-details.d.ts +33 -0
- package/dist/types/mcp/oauth-flow.d.ts +4 -6
- package/dist/types/modes/components/status-line/types.d.ts +10 -0
- package/dist/types/modes/controllers/tool-args-reveal.d.ts +9 -2
- package/dist/types/modes/theme/theme.d.ts +2 -1
- package/dist/types/session/agent-session.d.ts +11 -0
- package/dist/types/session/session-manager.d.ts +3 -4
- package/dist/types/task/provider-concurrency.d.ts +40 -0
- package/dist/types/utils/git.d.ts +11 -0
- package/dist/types/web/search/providers/duckduckgo.d.ts +2 -2
- package/dist/types/web/search/types.d.ts +1 -1
- package/package.json +12 -12
- package/src/cli/args.ts +32 -1
- package/src/cli/bench-cli.ts +89 -21
- package/src/cli/web-search-cli.ts +6 -1
- package/src/cli/worktree-cli.ts +8 -5
- package/src/commands/bench.ts +10 -1
- package/src/config/mcp-schema.json +1 -1
- package/src/config/model-discovery.ts +98 -20
- package/src/config/model-registry.ts +13 -6
- package/src/config/settings-schema.ts +5 -2
- package/src/edit/hashline/execute.ts +15 -9
- package/src/edit/index.ts +19 -6
- package/src/edit/modes/patch.ts +3 -2
- package/src/edit/modes/replace.ts +3 -2
- package/src/edit/renderer.ts +4 -0
- package/src/edit/snapshot-details.ts +77 -0
- package/src/extensibility/plugins/legacy-pi-compat.ts +2 -2
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/mcp/oauth-flow.ts +10 -8
- package/src/mcp/transports/stdio.ts +9 -17
- package/src/modes/components/status-line/component.ts +29 -2
- package/src/modes/components/status-line/segments.ts +22 -8
- package/src/modes/components/status-line/types.ts +7 -0
- package/src/modes/controllers/event-controller.ts +17 -8
- package/src/modes/controllers/input-controller.ts +1 -1
- package/src/modes/controllers/tool-args-reveal.ts +100 -22
- package/src/modes/theme/theme.ts +6 -0
- package/src/prompts/bench.md +4 -10
- package/src/prompts/tools/irc.md +19 -29
- package/src/prompts/tools/job.md +8 -14
- package/src/prompts/tools/lsp.md +19 -30
- package/src/prompts/tools/task.md +42 -62
- package/src/sdk.ts +14 -5
- package/src/session/agent-session.ts +107 -13
- package/src/session/session-listing.ts +9 -8
- package/src/session/session-loader.ts +98 -3
- package/src/session/session-manager.ts +34 -4
- package/src/subprocess/worker-client.ts +12 -4
- package/src/task/executor.ts +4 -62
- package/src/task/provider-concurrency.ts +100 -0
- package/src/task/worktree.ts +13 -4
- package/src/task/yield-assembly.ts +27 -39
- package/src/tools/path-utils.ts +4 -2
- package/src/tools/read.ts +11 -1
- package/src/utils/git.ts +13 -0
- package/src/web/search/index.ts +14 -8
- package/src/web/search/providers/duckduckgo.ts +136 -78
- package/src/web/search/providers/gemini.ts +268 -185
- package/src/web/search/types.ts +1 -1
package/src/web/search/index.ts
CHANGED
|
@@ -138,14 +138,20 @@ async function executeSearch(
|
|
|
138
138
|
options: ExecuteSearchOptions,
|
|
139
139
|
): Promise<{ content: Array<{ type: "text"; text: string }>; details: SearchRenderDetails }> {
|
|
140
140
|
const { authStorage, sessionId, signal } = options;
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
141
|
+
const explicitProvider = params.provider;
|
|
142
|
+
let providers: SearchProvider[];
|
|
143
|
+
if (explicitProvider && explicitProvider !== "auto") {
|
|
144
|
+
const provider = await getSearchProvider(explicitProvider);
|
|
145
|
+
providers = (await provider.isExplicitlyAvailable(authStorage))
|
|
146
|
+
? [provider]
|
|
147
|
+
: await resolveProviderChain(authStorage, "auto");
|
|
148
|
+
} else if (explicitProvider === "auto") {
|
|
149
|
+
// Explicit `--provider auto` bypasses the configured preferred provider
|
|
150
|
+
// for this invocation; exclusions still apply.
|
|
151
|
+
providers = await resolveProviderChain(authStorage, "auto");
|
|
152
|
+
} else {
|
|
153
|
+
providers = await resolveProviderChain(authStorage);
|
|
154
|
+
}
|
|
149
155
|
if (providers.length === 0) {
|
|
150
156
|
const message = "No web search provider configured.";
|
|
151
157
|
return {
|
|
@@ -6,122 +6,180 @@ import type { SearchParams } from "./base";
|
|
|
6
6
|
import { SearchProvider } from "./base";
|
|
7
7
|
import { classifyProviderHttpError, withHardTimeout } from "./utils";
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
/**
|
|
10
|
+
* DuckDuckGo's no-JS HTML search frontend. POST `q=…` to receive a static
|
|
11
|
+
* results page we can parse without a real browser. The Instant Answer API
|
|
12
|
+
* (`api.duckduckgo.com`) was tried first but it only returns content for
|
|
13
|
+
* Wikipedia/Wolfram-Alpha-style topics — empty for the vast majority of
|
|
14
|
+
* agent queries (see #3799).
|
|
15
|
+
*/
|
|
16
|
+
const DUCKDUCKGO_HTML_URL = "https://html.duckduckgo.com/html/";
|
|
10
17
|
const DEFAULT_NUM_RESULTS = 10;
|
|
11
18
|
const MAX_NUM_RESULTS = 20;
|
|
12
19
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Recency → DDG `df` form param. DDG accepts single letters for the time
|
|
22
|
+
* filter; queries without a `df` value return the unfiltered default.
|
|
23
|
+
*/
|
|
24
|
+
const RECENCY_TO_DDG_DF: Record<NonNullable<SearchParams["recency"]>, string> = {
|
|
25
|
+
day: "d",
|
|
26
|
+
week: "w",
|
|
27
|
+
month: "m",
|
|
28
|
+
year: "y",
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Browser-like UA so DDG serves the standard results page instead of the
|
|
33
|
+
* mobile-only or noscript variants. DDG returns HTTP 202 plus an anomaly
|
|
34
|
+
* modal when it suspects automation; we surface that as a clear error so
|
|
35
|
+
* the orchestrator can fall through to the next provider with context.
|
|
36
|
+
*/
|
|
37
|
+
const BROWSER_USER_AGENT =
|
|
38
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
|
|
39
|
+
|
|
40
|
+
interface ParsedResult {
|
|
41
|
+
title: string;
|
|
42
|
+
url: string;
|
|
43
|
+
snippet?: string;
|
|
17
44
|
}
|
|
18
45
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
function cleanText(value: string | null | undefined): string | undefined {
|
|
31
|
-
const cleaned = value
|
|
32
|
-
?.replace(/<[^>]*>/g, " ")
|
|
46
|
+
/**
|
|
47
|
+
* Decode an HTML-encoded fragment lifted from DDG markup. Strips inline tags
|
|
48
|
+
* (the results page wraps query terms in `<b>`), unescapes the small set of
|
|
49
|
+
* named entities DDG emits, and normalises whitespace.
|
|
50
|
+
*/
|
|
51
|
+
function decodeHtmlText(value: string): string {
|
|
52
|
+
return value
|
|
53
|
+
.replace(/<[^>]*>/g, " ")
|
|
54
|
+
.replace(/&#(\d+);/g, (_, code) => String.fromCharCode(Number(code)))
|
|
55
|
+
.replace(/&#x([0-9a-f]+);/gi, (_, code) => String.fromCharCode(Number.parseInt(code, 16)))
|
|
33
56
|
.replace(/ /gi, " ")
|
|
34
57
|
.replace(/&/gi, "&")
|
|
35
58
|
.replace(/</gi, "<")
|
|
36
59
|
.replace(/>/gi, ">")
|
|
37
60
|
.replace(/"/gi, '"')
|
|
38
|
-
.replace(/'/gi, "'")
|
|
61
|
+
.replace(/'|'/gi, "'")
|
|
39
62
|
.replace(/\s+/g, " ")
|
|
40
63
|
.trim();
|
|
41
|
-
return cleaned ? cleaned : undefined;
|
|
42
64
|
}
|
|
43
65
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
66
|
+
/**
|
|
67
|
+
* Resolve a DDG result href back to the underlying target URL.
|
|
68
|
+
*
|
|
69
|
+
* DDG routes outbound clicks through `//duckduckgo.com/l/?uddg=<encoded>` so
|
|
70
|
+
* it can record analytics; we want the unwrapped URL. Handles three shapes
|
|
71
|
+
* the page mixes in practice: redirect wrappers, protocol-relative links,
|
|
72
|
+
* and (rarely) absolute URLs on sponsored or instant answer rows.
|
|
73
|
+
*/
|
|
74
|
+
function unwrapResultUrl(href: string): string | undefined {
|
|
75
|
+
if (!href) return undefined;
|
|
76
|
+
const decoded = href.replace(/&/gi, "&");
|
|
77
|
+
const wrapMatch = decoded.match(/[?&]uddg=([^&]+)/);
|
|
78
|
+
if (wrapMatch) {
|
|
79
|
+
try {
|
|
80
|
+
return decodeURIComponent(wrapMatch[1]);
|
|
81
|
+
} catch {
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (decoded.startsWith("//")) return `https:${decoded}`;
|
|
86
|
+
if (decoded.startsWith("http://") || decoded.startsWith("https://")) return decoded;
|
|
87
|
+
return undefined;
|
|
47
88
|
}
|
|
48
89
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
90
|
+
/**
|
|
91
|
+
* Walk the HTML page and pull out result blocks in document order.
|
|
92
|
+
*
|
|
93
|
+
* DDG renders each result inside a `<div class="result …">` container with
|
|
94
|
+
* `<a class="result__a" …>` for the title link and an optional sibling
|
|
95
|
+
* `<a class="result__snippet">` (or `<div class="result__snippet">` in some
|
|
96
|
+
* variants) for the preview text. Sponsored placements, missing snippets,
|
|
97
|
+
* and the trailing pagination row are tolerated.
|
|
98
|
+
*/
|
|
99
|
+
function parseHtmlResults(html: string): ParsedResult[] {
|
|
100
|
+
const results: ParsedResult[] = [];
|
|
101
|
+
const blockRe =
|
|
102
|
+
/<div\b[^>]*\bclass="[^"]*\bresult\b[^"]*"[^>]*>([\s\S]*?)(?=<div\b[^>]*\bclass="[^"]*\bresult\b|<div\b[^>]*\bclass="[^"]*\bnav-link\b|$)/g;
|
|
103
|
+
const titleRe = /<a\b[^>]*\bclass="[^"]*\bresult__a\b[^"]*"[^>]*\bhref="([^"]+)"[^>]*>([\s\S]*?)<\/a>/;
|
|
104
|
+
const snippetRe = /<(?:a|div|span)\b[^>]*\bclass="[^"]*\bresult__snippet\b[^"]*"[^>]*>([\s\S]*?)<\/(?:a|div|span)>/;
|
|
105
|
+
for (const match of html.matchAll(blockRe)) {
|
|
106
|
+
const block = match[1];
|
|
107
|
+
const title = titleRe.exec(block);
|
|
108
|
+
if (!title) continue;
|
|
109
|
+
const url = unwrapResultUrl(title[1]);
|
|
110
|
+
if (!url) continue;
|
|
111
|
+
const titleText = decodeHtmlText(title[2]);
|
|
112
|
+
if (!titleText) continue;
|
|
113
|
+
const snippet = snippetRe.exec(block);
|
|
114
|
+
const snippetText = snippet ? decodeHtmlText(snippet[1]) : undefined;
|
|
115
|
+
results.push({ title: titleText, url, snippet: snippetText || undefined });
|
|
116
|
+
}
|
|
117
|
+
return results;
|
|
58
118
|
}
|
|
59
119
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
120
|
+
/**
|
|
121
|
+
* `true` when the page DDG returned is the bot-challenge modal instead of
|
|
122
|
+
* real results. DDG mixes status codes (200 vs 202) on these so the body
|
|
123
|
+
* check is the reliable signal.
|
|
124
|
+
*/
|
|
125
|
+
function isAnomalyResponse(html: string): boolean {
|
|
126
|
+
return html.includes("anomaly-modal") || html.includes("anomaly.js");
|
|
66
127
|
}
|
|
67
128
|
|
|
68
|
-
async function
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
129
|
+
async function callDuckDuckGoHtml(params: SearchParams): Promise<string> {
|
|
130
|
+
const form = new URLSearchParams({ q: params.query, kl: "us-en" });
|
|
131
|
+
const df = params.recency ? RECENCY_TO_DDG_DF[params.recency] : undefined;
|
|
132
|
+
if (df) form.set("df", df);
|
|
133
|
+
|
|
134
|
+
const response = await (params.fetch ?? fetch)(DUCKDUCKGO_HTML_URL, {
|
|
135
|
+
method: "POST",
|
|
136
|
+
body: form.toString(),
|
|
137
|
+
headers: {
|
|
138
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
139
|
+
"User-Agent": BROWSER_USER_AGENT,
|
|
140
|
+
Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
|
141
|
+
"Accept-Language": "en-US,en;q=0.5",
|
|
142
|
+
},
|
|
81
143
|
signal: withHardTimeout(params.signal),
|
|
82
144
|
});
|
|
83
145
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const classified = classifyProviderHttpError("duckduckgo", response.status,
|
|
146
|
+
const body = await response.text();
|
|
147
|
+
if (!response.ok && response.status !== 202) {
|
|
148
|
+
const classified = classifyProviderHttpError("duckduckgo", response.status, body);
|
|
87
149
|
if (classified) throw classified;
|
|
150
|
+
throw new SearchProviderError("duckduckgo", `DuckDuckGo HTML error (${response.status})`, response.status);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (isAnomalyResponse(body)) {
|
|
88
154
|
throw new SearchProviderError(
|
|
89
155
|
"duckduckgo",
|
|
90
|
-
|
|
91
|
-
|
|
156
|
+
"DuckDuckGo blocked the request with a bot-detection challenge. DuckDuckGo throttles repeat searches from datacenter and shared-egress IPs; configure another provider (e.g. Brave, Tavily, SearXNG) or retry from a residential network.",
|
|
157
|
+
429,
|
|
92
158
|
);
|
|
93
159
|
}
|
|
94
160
|
|
|
95
|
-
return
|
|
161
|
+
return body;
|
|
96
162
|
}
|
|
97
163
|
|
|
98
|
-
/** Execute DuckDuckGo
|
|
164
|
+
/** Execute a DuckDuckGo web search via the no-JS HTML frontend. */
|
|
99
165
|
export async function searchDuckDuckGo(params: SearchParams): Promise<SearchResponse> {
|
|
100
166
|
const numResults = clampNumResults(params.numSearchResults ?? params.limit, DEFAULT_NUM_RESULTS, MAX_NUM_RESULTS);
|
|
101
|
-
const
|
|
102
|
-
const
|
|
103
|
-
const sources: SearchSource[] = [];
|
|
167
|
+
const html = await callDuckDuckGoHtml(params);
|
|
168
|
+
const parsed = parseHtmlResults(html);
|
|
104
169
|
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
170
|
+
const sources: SearchSource[] = [];
|
|
171
|
+
const seen = new Set<string>();
|
|
172
|
+
for (const result of parsed) {
|
|
173
|
+
if (seen.has(result.url)) continue;
|
|
174
|
+
seen.add(result.url);
|
|
175
|
+
sources.push({ title: result.title, url: result.url, snippet: result.snippet });
|
|
176
|
+
if (sources.length >= numResults) break;
|
|
112
177
|
}
|
|
113
178
|
|
|
114
|
-
|
|
115
|
-
collectTopicSources(sources, data.RelatedTopics);
|
|
116
|
-
|
|
117
|
-
return {
|
|
118
|
-
provider: "duckduckgo",
|
|
119
|
-
answer,
|
|
120
|
-
sources: sources.slice(0, numResults),
|
|
121
|
-
};
|
|
179
|
+
return { provider: "duckduckgo", sources };
|
|
122
180
|
}
|
|
123
181
|
|
|
124
|
-
/** Search provider for DuckDuckGo
|
|
182
|
+
/** Search provider for DuckDuckGo (no API key required). */
|
|
125
183
|
export class DuckDuckGoProvider extends SearchProvider {
|
|
126
184
|
readonly id = "duckduckgo";
|
|
127
185
|
readonly label = "DuckDuckGo";
|