@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.
Files changed (68) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/dist/cli.js +3211 -3231
  3. package/dist/types/cli/bench-cli.d.ts +6 -1
  4. package/dist/types/commands/bench.d.ts +8 -0
  5. package/dist/types/config/model-discovery.d.ts +6 -1
  6. package/dist/types/config/settings-schema.d.ts +1 -1
  7. package/dist/types/edit/index.d.ts +1 -0
  8. package/dist/types/edit/renderer.d.ts +4 -0
  9. package/dist/types/edit/snapshot-details.d.ts +33 -0
  10. package/dist/types/mcp/oauth-flow.d.ts +4 -6
  11. package/dist/types/modes/components/status-line/types.d.ts +10 -0
  12. package/dist/types/modes/controllers/tool-args-reveal.d.ts +9 -2
  13. package/dist/types/modes/theme/theme.d.ts +2 -1
  14. package/dist/types/session/agent-session.d.ts +11 -0
  15. package/dist/types/session/session-manager.d.ts +3 -4
  16. package/dist/types/task/provider-concurrency.d.ts +40 -0
  17. package/dist/types/utils/git.d.ts +11 -0
  18. package/dist/types/web/search/providers/duckduckgo.d.ts +2 -2
  19. package/dist/types/web/search/types.d.ts +1 -1
  20. package/package.json +12 -12
  21. package/src/cli/args.ts +32 -1
  22. package/src/cli/bench-cli.ts +89 -21
  23. package/src/cli/web-search-cli.ts +6 -1
  24. package/src/cli/worktree-cli.ts +8 -5
  25. package/src/commands/bench.ts +10 -1
  26. package/src/config/mcp-schema.json +1 -1
  27. package/src/config/model-discovery.ts +98 -20
  28. package/src/config/model-registry.ts +13 -6
  29. package/src/config/settings-schema.ts +5 -2
  30. package/src/edit/hashline/execute.ts +15 -9
  31. package/src/edit/index.ts +19 -6
  32. package/src/edit/modes/patch.ts +3 -2
  33. package/src/edit/modes/replace.ts +3 -2
  34. package/src/edit/renderer.ts +4 -0
  35. package/src/edit/snapshot-details.ts +77 -0
  36. package/src/extensibility/plugins/legacy-pi-compat.ts +2 -2
  37. package/src/internal-urls/docs-index.generated.txt +1 -1
  38. package/src/mcp/oauth-flow.ts +10 -8
  39. package/src/mcp/transports/stdio.ts +9 -17
  40. package/src/modes/components/status-line/component.ts +29 -2
  41. package/src/modes/components/status-line/segments.ts +22 -8
  42. package/src/modes/components/status-line/types.ts +7 -0
  43. package/src/modes/controllers/event-controller.ts +17 -8
  44. package/src/modes/controllers/input-controller.ts +1 -1
  45. package/src/modes/controllers/tool-args-reveal.ts +100 -22
  46. package/src/modes/theme/theme.ts +6 -0
  47. package/src/prompts/bench.md +4 -10
  48. package/src/prompts/tools/irc.md +19 -29
  49. package/src/prompts/tools/job.md +8 -14
  50. package/src/prompts/tools/lsp.md +19 -30
  51. package/src/prompts/tools/task.md +42 -62
  52. package/src/sdk.ts +14 -5
  53. package/src/session/agent-session.ts +107 -13
  54. package/src/session/session-listing.ts +9 -8
  55. package/src/session/session-loader.ts +98 -3
  56. package/src/session/session-manager.ts +34 -4
  57. package/src/subprocess/worker-client.ts +12 -4
  58. package/src/task/executor.ts +4 -62
  59. package/src/task/provider-concurrency.ts +100 -0
  60. package/src/task/worktree.ts +13 -4
  61. package/src/task/yield-assembly.ts +27 -39
  62. package/src/tools/path-utils.ts +4 -2
  63. package/src/tools/read.ts +11 -1
  64. package/src/utils/git.ts +13 -0
  65. package/src/web/search/index.ts +14 -8
  66. package/src/web/search/providers/duckduckgo.ts +136 -78
  67. package/src/web/search/providers/gemini.ts +268 -185
  68. package/src/web/search/types.ts +1 -1
@@ -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 providers =
142
- params.provider && params.provider !== "auto"
143
- ? await getSearchProvider(params.provider).then(async provider =>
144
- (await provider.isExplicitlyAvailable(authStorage))
145
- ? [provider]
146
- : resolveProviderChain(authStorage, "auto"),
147
- )
148
- : await resolveProviderChain(authStorage);
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
- const DUCKDUCKGO_SEARCH_URL = "https://api.duckduckgo.com/";
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
- interface DuckDuckGoTopic {
14
- FirstURL?: string | null;
15
- Text?: string | null;
16
- Topics?: DuckDuckGoTopic[] | null;
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
- interface DuckDuckGoResponse {
20
- AbstractText?: string | null;
21
- AbstractURL?: string | null;
22
- AbstractSource?: string | null;
23
- Answer?: string | null;
24
- Definition?: string | null;
25
- Heading?: string | null;
26
- Results?: DuckDuckGoTopic[] | null;
27
- RelatedTopics?: DuckDuckGoTopic[] | null;
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(/&nbsp;/gi, " ")
34
57
  .replace(/&amp;/gi, "&")
35
58
  .replace(/&lt;/gi, "<")
36
59
  .replace(/&gt;/gi, ">")
37
60
  .replace(/&quot;/gi, '"')
38
- .replace(/&#39;/gi, "'")
61
+ .replace(/&#39;|&apos;/gi, "'")
39
62
  .replace(/\s+/g, " ")
40
63
  .trim();
41
- return cleaned ? cleaned : undefined;
42
64
  }
43
65
 
44
- function addSource(sources: SearchSource[], source: SearchSource): void {
45
- if (!source.url || sources.some(existing => existing.url === source.url)) return;
46
- sources.push(source);
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(/&amp;/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
- function addTopicSource(sources: SearchSource[], topic: DuckDuckGoTopic): void {
50
- const url = topic.FirstURL?.trim();
51
- if (!url) return;
52
- const text = cleanText(topic.Text);
53
- addSource(sources, {
54
- title: text ?? url,
55
- url,
56
- snippet: text,
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
- function collectTopicSources(sources: SearchSource[], topics: readonly DuckDuckGoTopic[] | null | undefined): void {
61
- if (!topics) return;
62
- for (const topic of topics) {
63
- addTopicSource(sources, topic);
64
- collectTopicSources(sources, topic.Topics);
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 callDuckDuckGoSearch(params: SearchParams): Promise<DuckDuckGoResponse> {
69
- const queryString = [
70
- ["q", params.query],
71
- ["format", "json"],
72
- ["no_redirect", "1"],
73
- ["no_html", "1"],
74
- ["skip_disambig", "1"],
75
- ["t", "oh-my-pi"],
76
- ]
77
- .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
78
- .join("&");
79
- const response = await (params.fetch ?? fetch)(`${DUCKDUCKGO_SEARCH_URL}?${queryString}`, {
80
- method: "GET",
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
- if (!response.ok) {
85
- const errorText = await response.text();
86
- const classified = classifyProviderHttpError("duckduckgo", response.status, errorText);
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
- `DuckDuckGo API error (${response.status}): ${errorText}`,
91
- response.status,
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 (await response.json()) as DuckDuckGoResponse;
161
+ return body;
96
162
  }
97
163
 
98
- /** Execute DuckDuckGo Instant Answer API search. */
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 data = await callDuckDuckGoSearch(params);
102
- const answer = cleanText(data.AbstractText) ?? cleanText(data.Answer) ?? cleanText(data.Definition);
103
- const sources: SearchSource[] = [];
167
+ const html = await callDuckDuckGoHtml(params);
168
+ const parsed = parseHtmlResults(html);
104
169
 
105
- const abstractUrl = data.AbstractURL?.trim();
106
- if (abstractUrl) {
107
- addSource(sources, {
108
- title: cleanText(data.AbstractSource) ?? cleanText(data.Heading) ?? abstractUrl,
109
- url: abstractUrl,
110
- snippet: cleanText(data.AbstractText),
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
- collectTopicSources(sources, data.Results);
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 Instant Answer API. */
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";