@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 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",
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.4",
45
- "@oh-my-pi/pi-agent-core": "13.5.4",
46
- "@oh-my-pi/pi-ai": "13.5.4",
47
- "@oh-my-pi/pi-natives": "13.5.4",
48
- "@oh-my-pi/pi-tui": "13.5.4",
49
- "@oh-my-pi/pi-utils": "13.5.4",
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",
@@ -757,6 +757,7 @@ export const SETTINGS_SCHEMA = {
757
757
  "anthropic",
758
758
  "gemini",
759
759
  "codex",
760
+ "kagi",
760
761
  "synthetic",
761
762
  ] as const,
762
763
  default: "auto",
@@ -181,7 +181,7 @@ export class MCPOAuthFlow extends OAuthCallbackFlow {
181
181
  Accept: "application/json",
182
182
  },
183
183
  body: JSON.stringify({
184
- client_name: "oh-my-pi MCP",
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);
@@ -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
- ["auto", "exa", "brave", "jina", "kimi", "zai", "anthropic", "perplexity", "gemini", "codex", "synthetic"],
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
+ }
@@ -15,6 +15,7 @@ export type SearchProviderId =
15
15
  | "perplexity"
16
16
  | "gemini"
17
17
  | "codex"
18
+ | "kagi"
18
19
  | "synthetic";
19
20
 
20
21
  /** Source returned by search (all providers) */