@oh-my-pi/pi-coding-agent 13.5.4 → 13.5.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 +14 -0
- package/package.json +7 -7
- package/src/config/settings-schema.ts +1 -0
- package/src/mcp/oauth-flow.ts +1 -1
- package/src/modes/components/settings-defs.ts +1 -0
- package/src/modes/interactive-mode.ts +6 -0
- package/src/web/search/index.ts +15 -1
- package/src/web/search/provider.ts +3 -0
- package/src/web/search/providers/kagi.ts +148 -0
- package/src/web/search/types.ts +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [13.5.6] - 2026-03-01
|
|
6
|
+
### Changed
|
|
7
|
+
|
|
8
|
+
- Updated OAuth client name from 'oh-my-pi MCP' to 'Codex' for dynamic client registration
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- Fixed exit_plan_mode handler to abort active agent turn before opening plan approval selector, ensuring proper session cleanup
|
|
12
|
+
|
|
13
|
+
## [13.5.5] - 2026-03-01
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- Added Kagi web search provider (Search API v0) with related searches support and automatic `KAGI_API_KEY` detection
|
|
18
|
+
|
|
5
19
|
## [13.5.4] - 2026-03-01
|
|
6
20
|
### Added
|
|
7
21
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
4
|
-
"version": "13.5.
|
|
4
|
+
"version": "13.5.6",
|
|
5
5
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://github.com/can1357/oh-my-pi",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -41,12 +41,12 @@
|
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"@mozilla/readability": "^0.6",
|
|
44
|
-
"@oh-my-pi/omp-stats": "13.5.
|
|
45
|
-
"@oh-my-pi/pi-agent-core": "13.5.
|
|
46
|
-
"@oh-my-pi/pi-ai": "13.5.
|
|
47
|
-
"@oh-my-pi/pi-natives": "13.5.
|
|
48
|
-
"@oh-my-pi/pi-tui": "13.5.
|
|
49
|
-
"@oh-my-pi/pi-utils": "13.5.
|
|
44
|
+
"@oh-my-pi/omp-stats": "13.5.6",
|
|
45
|
+
"@oh-my-pi/pi-agent-core": "13.5.6",
|
|
46
|
+
"@oh-my-pi/pi-ai": "13.5.6",
|
|
47
|
+
"@oh-my-pi/pi-natives": "13.5.6",
|
|
48
|
+
"@oh-my-pi/pi-tui": "13.5.6",
|
|
49
|
+
"@oh-my-pi/pi-utils": "13.5.6",
|
|
50
50
|
"@sinclair/typebox": "^0.34",
|
|
51
51
|
"@xterm/headless": "^6.0",
|
|
52
52
|
"ajv": "^8.18",
|
package/src/mcp/oauth-flow.ts
CHANGED
|
@@ -181,7 +181,7 @@ export class MCPOAuthFlow extends OAuthCallbackFlow {
|
|
|
181
181
|
Accept: "application/json",
|
|
182
182
|
},
|
|
183
183
|
body: JSON.stringify({
|
|
184
|
-
client_name: "
|
|
184
|
+
client_name: "Codex",
|
|
185
185
|
redirect_uris: [redirectUri],
|
|
186
186
|
grant_types: ["authorization_code", "refresh_token"],
|
|
187
187
|
response_types: ["code"],
|
|
@@ -194,6 +194,7 @@ const OPTION_PROVIDERS: Partial<Record<SettingPath, OptionProvider>> = {
|
|
|
194
194
|
{ value: "perplexity", label: "Perplexity", description: "Requires PERPLEXITY_COOKIES or PERPLEXITY_API_KEY" },
|
|
195
195
|
{ value: "anthropic", label: "Anthropic", description: "Uses Anthropic web search" },
|
|
196
196
|
{ value: "zai", label: "Z.AI", description: "Calls Z.AI webSearchPrime MCP" },
|
|
197
|
+
{ value: "kagi", label: "Kagi", description: "Requires KAGI_API_KEY" },
|
|
197
198
|
{ value: "synthetic", label: "Synthetic", description: "Requires SYNTHETIC_API_KEY" },
|
|
198
199
|
],
|
|
199
200
|
"providers.image": [
|
|
@@ -724,6 +724,12 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
724
724
|
return;
|
|
725
725
|
}
|
|
726
726
|
|
|
727
|
+
// Abort the agent to prevent it from continuing (e.g., calling exit_plan_mode
|
|
728
|
+
// again) while the popup is showing. The event listener fires asynchronously
|
|
729
|
+
// (agent's #emit is fire-and-forget), so without this the model sees "Plan
|
|
730
|
+
// ready for approval." and immediately calls exit_plan_mode in a loop.
|
|
731
|
+
await this.session.abort();
|
|
732
|
+
|
|
727
733
|
const planFilePath = details.planFilePath || this.planModePlanFilePath || (await this.#getPlanFilePath());
|
|
728
734
|
this.planModePlanFilePath = planFilePath;
|
|
729
735
|
const planContent = await this.#readPlanFile(planFilePath);
|
package/src/web/search/index.ts
CHANGED
|
@@ -34,7 +34,20 @@ export const webSearchSchema = Type.Object({
|
|
|
34
34
|
query: Type.String({ description: "Search query" }),
|
|
35
35
|
provider: Type.Optional(
|
|
36
36
|
StringEnum(
|
|
37
|
-
[
|
|
37
|
+
[
|
|
38
|
+
"auto",
|
|
39
|
+
"exa",
|
|
40
|
+
"brave",
|
|
41
|
+
"jina",
|
|
42
|
+
"kimi",
|
|
43
|
+
"zai",
|
|
44
|
+
"anthropic",
|
|
45
|
+
"perplexity",
|
|
46
|
+
"gemini",
|
|
47
|
+
"codex",
|
|
48
|
+
"kagi",
|
|
49
|
+
"synthetic",
|
|
50
|
+
],
|
|
38
51
|
{
|
|
39
52
|
description: "Search provider (default: auto)",
|
|
40
53
|
},
|
|
@@ -64,6 +77,7 @@ export type SearchParams = {
|
|
|
64
77
|
| "perplexity"
|
|
65
78
|
| "gemini"
|
|
66
79
|
| "codex"
|
|
80
|
+
| "kagi"
|
|
67
81
|
| "synthetic";
|
|
68
82
|
recency?: "day" | "week" | "month" | "year";
|
|
69
83
|
limit?: number;
|
|
@@ -5,6 +5,7 @@ import { CodexProvider } from "./providers/codex";
|
|
|
5
5
|
import { ExaProvider } from "./providers/exa";
|
|
6
6
|
import { GeminiProvider } from "./providers/gemini";
|
|
7
7
|
import { JinaProvider } from "./providers/jina";
|
|
8
|
+
import { KagiProvider } from "./providers/kagi";
|
|
8
9
|
import { KimiProvider } from "./providers/kimi";
|
|
9
10
|
import { PerplexityProvider } from "./providers/perplexity";
|
|
10
11
|
import { SyntheticProvider } from "./providers/synthetic";
|
|
@@ -24,6 +25,7 @@ const SEARCH_PROVIDERS: Record<SearchProviderId, SearchProvider> = {
|
|
|
24
25
|
anthropic: new AnthropicProvider(),
|
|
25
26
|
gemini: new GeminiProvider(),
|
|
26
27
|
codex: new CodexProvider(),
|
|
28
|
+
kagi: new KagiProvider(),
|
|
27
29
|
synthetic: new SyntheticProvider(),
|
|
28
30
|
} as const;
|
|
29
31
|
|
|
@@ -37,6 +39,7 @@ export const SEARCH_PROVIDER_ORDER: SearchProviderId[] = [
|
|
|
37
39
|
"codex",
|
|
38
40
|
"zai",
|
|
39
41
|
"exa",
|
|
42
|
+
"kagi",
|
|
40
43
|
"synthetic",
|
|
41
44
|
];
|
|
42
45
|
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kagi Web Search Provider
|
|
3
|
+
*
|
|
4
|
+
* Calls Kagi's Search API (v0) and maps results into the unified
|
|
5
|
+
* SearchResponse shape used by the web search tool.
|
|
6
|
+
*/
|
|
7
|
+
import { getEnvApiKey } from "@oh-my-pi/pi-ai";
|
|
8
|
+
import type { SearchResponse, SearchSource } from "../../../web/search/types";
|
|
9
|
+
import { SearchProviderError } from "../../../web/search/types";
|
|
10
|
+
import { clampNumResults, dateToAgeSeconds } from "../utils";
|
|
11
|
+
import type { SearchParams } from "./base";
|
|
12
|
+
import { SearchProvider } from "./base";
|
|
13
|
+
|
|
14
|
+
const KAGI_SEARCH_URL = "https://kagi.com/api/v0/search";
|
|
15
|
+
const DEFAULT_NUM_RESULTS = 10;
|
|
16
|
+
const MAX_NUM_RESULTS = 40;
|
|
17
|
+
|
|
18
|
+
interface KagiSearchResult {
|
|
19
|
+
t: 0;
|
|
20
|
+
url: string;
|
|
21
|
+
title: string;
|
|
22
|
+
snippet?: string;
|
|
23
|
+
published?: string;
|
|
24
|
+
thumbnail?: {
|
|
25
|
+
url: string;
|
|
26
|
+
width?: number | null;
|
|
27
|
+
height?: number | null;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface KagiRelatedSearches {
|
|
32
|
+
t: 1;
|
|
33
|
+
list: string[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
type KagiSearchObject = KagiSearchResult | KagiRelatedSearches;
|
|
37
|
+
|
|
38
|
+
interface KagiMeta {
|
|
39
|
+
id: string;
|
|
40
|
+
node: string;
|
|
41
|
+
ms: number;
|
|
42
|
+
api_balance?: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface KagiSearchResponse {
|
|
46
|
+
meta: KagiMeta;
|
|
47
|
+
data: KagiSearchObject[];
|
|
48
|
+
error?: Array<{ code: number; msg: string; ref?: unknown }>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Find KAGI_API_KEY from environment or .env files. */
|
|
52
|
+
export function findApiKey(): string | null {
|
|
53
|
+
return getEnvApiKey("kagi") ?? null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function callKagiSearch(
|
|
57
|
+
apiKey: string,
|
|
58
|
+
query: string,
|
|
59
|
+
limit: number,
|
|
60
|
+
signal?: AbortSignal,
|
|
61
|
+
): Promise<KagiSearchResponse> {
|
|
62
|
+
const url = new URL(KAGI_SEARCH_URL);
|
|
63
|
+
url.searchParams.set("q", query);
|
|
64
|
+
url.searchParams.set("limit", String(limit));
|
|
65
|
+
|
|
66
|
+
const response = await fetch(url, {
|
|
67
|
+
headers: {
|
|
68
|
+
Authorization: `Bot ${apiKey}`,
|
|
69
|
+
Accept: "application/json",
|
|
70
|
+
},
|
|
71
|
+
signal,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
if (!response.ok) {
|
|
75
|
+
const errorText = await response.text();
|
|
76
|
+
throw new SearchProviderError("kagi", `Kagi API error (${response.status}): ${errorText}`, response.status);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const data = (await response.json()) as KagiSearchResponse;
|
|
80
|
+
|
|
81
|
+
if (data.error && data.error.length > 0) {
|
|
82
|
+
const firstError = data.error[0];
|
|
83
|
+
throw new SearchProviderError("kagi", `Kagi API error: ${firstError.msg}`, firstError.code);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return data;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Execute Kagi web search. */
|
|
90
|
+
export async function searchKagi(params: {
|
|
91
|
+
query: string;
|
|
92
|
+
num_results?: number;
|
|
93
|
+
signal?: AbortSignal;
|
|
94
|
+
}): Promise<SearchResponse> {
|
|
95
|
+
const numResults = clampNumResults(params.num_results, DEFAULT_NUM_RESULTS, MAX_NUM_RESULTS);
|
|
96
|
+
const apiKey = findApiKey();
|
|
97
|
+
if (!apiKey) {
|
|
98
|
+
throw new Error("KAGI_API_KEY not found. Set it in environment or .env file.");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const data = await callKagiSearch(apiKey, params.query, numResults, params.signal);
|
|
102
|
+
|
|
103
|
+
const sources: SearchSource[] = [];
|
|
104
|
+
const relatedQuestions: string[] = [];
|
|
105
|
+
|
|
106
|
+
for (const item of data.data) {
|
|
107
|
+
if (item.t === 0) {
|
|
108
|
+
sources.push({
|
|
109
|
+
title: item.title,
|
|
110
|
+
url: item.url,
|
|
111
|
+
snippet: item.snippet,
|
|
112
|
+
publishedDate: item.published ?? undefined,
|
|
113
|
+
ageSeconds: dateToAgeSeconds(item.published),
|
|
114
|
+
});
|
|
115
|
+
} else if (item.t === 1) {
|
|
116
|
+
relatedQuestions.push(...item.list);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
provider: "kagi",
|
|
122
|
+
sources: sources.slice(0, numResults),
|
|
123
|
+
relatedQuestions: relatedQuestions.length > 0 ? relatedQuestions : undefined,
|
|
124
|
+
requestId: data.meta.id,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Search provider for Kagi web search. */
|
|
129
|
+
export class KagiProvider extends SearchProvider {
|
|
130
|
+
readonly id = "kagi";
|
|
131
|
+
readonly label = "Kagi";
|
|
132
|
+
|
|
133
|
+
isAvailable() {
|
|
134
|
+
try {
|
|
135
|
+
return !!findApiKey();
|
|
136
|
+
} catch {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
search(params: SearchParams): Promise<SearchResponse> {
|
|
142
|
+
return searchKagi({
|
|
143
|
+
query: params.query,
|
|
144
|
+
num_results: params.numSearchResults ?? params.limit,
|
|
145
|
+
signal: params.signal,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|