@oh-my-pi/pi-coding-agent 12.12.0 → 12.12.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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [12.12.1] - 2026-02-19
6
+
7
+ ### Added
8
+
9
+ - Added Kimi (Moonshot) as a web search provider with OAuth and API key support ([#110](https://github.com/can1357/oh-my-pi/pull/110) by [@oglassdev](https://github.com/oglassdev))
10
+
11
+ ### Changed
12
+
13
+ - Changed web search auto-resolve priority to prefer Perplexity first
14
+
5
15
  ### Fixed
6
16
 
7
17
  - Fixed Mermaid pre-render failures from repeatedly re-triggering background renders (freeze loop) and restored resilient rendering when diagram conversion/callbacks fail ([#109](https://github.com/can1357/oh-my-pi/issues/109)).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-coding-agent",
3
- "version": "12.12.0",
3
+ "version": "12.12.1",
4
4
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
5
5
  "type": "module",
6
6
  "bin": {
@@ -84,12 +84,12 @@
84
84
  },
85
85
  "dependencies": {
86
86
  "@mozilla/readability": "0.6.0",
87
- "@oh-my-pi/omp-stats": "12.12.0",
88
- "@oh-my-pi/pi-agent-core": "12.12.0",
89
- "@oh-my-pi/pi-ai": "12.12.0",
90
- "@oh-my-pi/pi-natives": "12.12.0",
91
- "@oh-my-pi/pi-tui": "12.12.0",
92
- "@oh-my-pi/pi-utils": "12.12.0",
87
+ "@oh-my-pi/omp-stats": "12.12.1",
88
+ "@oh-my-pi/pi-agent-core": "12.12.1",
89
+ "@oh-my-pi/pi-ai": "12.12.1",
90
+ "@oh-my-pi/pi-natives": "12.12.1",
91
+ "@oh-my-pi/pi-tui": "12.12.1",
92
+ "@oh-my-pi/pi-utils": "12.12.1",
93
93
  "@sinclair/typebox": "^0.34.48",
94
94
  "@xterm/headless": "^6.0.0",
95
95
  "ajv": "^8.18.0",
@@ -8,6 +8,7 @@ import { APP_NAME } from "@oh-my-pi/pi-utils/dirs";
8
8
  import chalk from "chalk";
9
9
  import { initTheme, theme } from "../modes/theme/theme";
10
10
  import { runSearchQuery, type SearchParams } from "../web/search/index";
11
+ import { SEARCH_PROVIDER_ORDER } from "../web/search/provider";
11
12
  import { renderSearchResult } from "../web/search/render";
12
13
  import type { SearchProviderId } from "../web/search/types";
13
14
 
@@ -19,18 +20,7 @@ export interface SearchCommandArgs {
19
20
  expanded: boolean;
20
21
  }
21
22
 
22
- const PROVIDERS: Array<SearchProviderId | "auto"> = [
23
- "auto",
24
- "anthropic",
25
- "perplexity",
26
- "exa",
27
- "brave",
28
- "jina",
29
- "zai",
30
- "gemini",
31
- "codex",
32
- "synthetic",
33
- ];
23
+ const PROVIDERS: Array<SearchProviderId | "auto"> = ["auto", ...SEARCH_PROVIDER_ORDER];
34
24
 
35
25
  const RECENCY_OPTIONS: SearchCommandArgs["recency"][] = ["day", "week", "month", "year"];
36
26
 
@@ -3,20 +3,9 @@
3
3
  */
4
4
  import { Args, Command, Flags } from "@oh-my-pi/pi-utils/cli";
5
5
  import { runSearchCommand, type SearchCommandArgs } from "../cli/web-search-cli";
6
- import type { SearchProviderId } from "../web/search/types";
7
-
8
- const PROVIDERS: Array<SearchProviderId | "auto"> = [
9
- "auto",
10
- "anthropic",
11
- "perplexity",
12
- "exa",
13
- "brave",
14
- "jina",
15
- "zai",
16
- "gemini",
17
- "codex",
18
- "synthetic",
19
- ];
6
+ import { SEARCH_PROVIDER_ORDER } from "../web/search/provider";
7
+
8
+ const PROVIDERS: Array<string> = ["auto", ...SEARCH_PROVIDER_ORDER];
20
9
 
21
10
  const RECENCY: NonNullable<SearchCommandArgs["recency"]>[] = ["day", "week", "month", "year"];
22
11
 
@@ -42,7 +31,7 @@ export default class Search extends Command {
42
31
 
43
32
  const cmd: SearchCommandArgs = {
44
33
  query,
45
- provider: flags.provider as SearchProviderId | "auto" | undefined,
34
+ provider: flags.provider as SearchCommandArgs["provider"],
46
35
  recency: flags.recency as SearchCommandArgs["recency"],
47
36
  limit: flags.limit,
48
37
  expanded: !flags.compact,
@@ -653,7 +653,7 @@ export const SETTINGS_SCHEMA = {
653
653
  // ─────────────────────────────────────────────────────────────────────────
654
654
  "providers.webSearch": {
655
655
  type: "enum",
656
- values: ["auto", "exa", "brave", "jina", "zai", "perplexity", "anthropic"] as const,
656
+ values: ["auto", "exa", "brave", "jina", "kimi", "zai", "perplexity", "anthropic"] as const,
657
657
  default: "auto",
658
658
  ui: { tab: "services", label: "Web search provider", description: "Provider for web search tool", submenu: true },
659
659
  },
@@ -164,11 +164,13 @@ const OPTION_PROVIDERS: Partial<Record<SettingPath, OptionProvider>> = {
164
164
  {
165
165
  value: "auto",
166
166
  label: "Auto",
167
- description: "Priority: Exa > Brave > Jina > Perplexity > Anthropic > Gemini > Codex > Z.AI > Synthetic",
167
+ description:
168
+ "Priority: Perplexity > Exa > Brave > Jina > Kimi > Anthropic > Gemini > Codex > Z.AI > Synthetic",
168
169
  },
169
170
  { value: "exa", label: "Exa", description: "Requires EXA_API_KEY" },
170
171
  { value: "brave", label: "Brave", description: "Requires BRAVE_API_KEY" },
171
172
  { value: "jina", label: "Jina", description: "Requires JINA_API_KEY" },
173
+ { value: "kimi", label: "Kimi", description: "Requires MOONSHOT_SEARCH_API_KEY or MOONSHOT_API_KEY" },
172
174
  { value: "perplexity", label: "Perplexity", description: "Requires PERPLEXITY_API_KEY" },
173
175
  { value: "anthropic", label: "Anthropic", description: "Uses Anthropic web search" },
174
176
  { value: "zai", label: "Z.AI", description: "Calls Z.AI webSearchPrime MCP" },
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Unified Web Search Tool
3
3
  *
4
- * Single tool supporting Anthropic, Perplexity, Exa, Brave, Jina, Gemini, Codex, Z.AI, and Synthetic
4
+ * Single tool supporting Anthropic, Perplexity, Exa, Brave, Jina, Kimi, Gemini, Codex, Z.AI, and Synthetic
5
5
  * providers with provider-specific parameters exposed conditionally.
6
6
  *
7
7
  * When EXA_API_KEY is available, additional specialized tools are exposed:
@@ -33,9 +33,12 @@ import { SearchProviderError } from "./types";
33
33
  export const webSearchSchema = Type.Object({
34
34
  query: Type.String({ description: "Search query" }),
35
35
  provider: Type.Optional(
36
- StringEnum(["auto", "exa", "brave", "jina", "zai", "anthropic", "perplexity", "gemini", "codex", "synthetic"], {
37
- description: "Search provider (default: auto)",
38
- }),
36
+ StringEnum(
37
+ ["auto", "exa", "brave", "jina", "kimi", "zai", "anthropic", "perplexity", "gemini", "codex", "synthetic"],
38
+ {
39
+ description: "Search provider (default: auto)",
40
+ },
41
+ ),
39
42
  ),
40
43
  recency: Type.Optional(
41
44
  StringEnum(["day", "week", "month", "year"], {
@@ -47,7 +50,18 @@ export const webSearchSchema = Type.Object({
47
50
 
48
51
  export type SearchParams = {
49
52
  query: string;
50
- provider?: "auto" | "exa" | "brave" | "jina" | "zai" | "anthropic" | "perplexity" | "gemini" | "codex" | "synthetic";
53
+ provider?:
54
+ | "auto"
55
+ | "exa"
56
+ | "brave"
57
+ | "jina"
58
+ | "kimi"
59
+ | "zai"
60
+ | "anthropic"
61
+ | "perplexity"
62
+ | "gemini"
63
+ | "codex"
64
+ | "synthetic";
51
65
  recency?: "day" | "week" | "month" | "year";
52
66
  limit?: number;
53
67
  /** Maximum output tokens. Defaults to 4096. */
@@ -236,7 +250,7 @@ export async function runSearchQuery(
236
250
  /**
237
251
  * Web search tool implementation.
238
252
  *
239
- * Supports Anthropic, Perplexity, Exa, Brave, Jina, Gemini, Codex, Z.AI, and Synthetic providers with automatic fallback.
253
+ * Supports Anthropic, Perplexity, Exa, Brave, Jina, Kimi, Gemini, Codex, Z.AI, and Synthetic providers with automatic fallback.
240
254
  * Session is accepted for interface consistency but not used.
241
255
  */
242
256
  export class SearchTool implements AgentTool<typeof webSearchSchema, SearchRenderDetails> {
@@ -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 { KimiProvider } from "./providers/kimi";
8
9
  import { PerplexityProvider } from "./providers/perplexity";
9
10
  import { SyntheticProvider } from "./providers/synthetic";
10
11
  import { ZaiProvider } from "./providers/zai";
@@ -18,6 +19,7 @@ const SEARCH_PROVIDERS: Record<SearchProviderId, SearchProvider> = {
18
19
  brave: new BraveProvider(),
19
20
  jina: new JinaProvider(),
20
21
  perplexity: new PerplexityProvider(),
22
+ kimi: new KimiProvider(),
21
23
  zai: new ZaiProvider(),
22
24
  anthropic: new AnthropicProvider(),
23
25
  gemini: new GeminiProvider(),
@@ -25,11 +27,12 @@ const SEARCH_PROVIDERS: Record<SearchProviderId, SearchProvider> = {
25
27
  synthetic: new SyntheticProvider(),
26
28
  } as const;
27
29
 
28
- const SEARCH_PROVIDER_ORDER: SearchProviderId[] = [
30
+ export const SEARCH_PROVIDER_ORDER: SearchProviderId[] = [
31
+ "perplexity",
29
32
  "exa",
30
33
  "brave",
31
34
  "jina",
32
- "perplexity",
35
+ "kimi",
33
36
  "anthropic",
34
37
  "gemini",
35
38
  "codex",
@@ -49,7 +52,7 @@ export function setPreferredSearchProvider(provider: SearchProviderId | "auto"):
49
52
  preferredProvId = provider;
50
53
  }
51
54
 
52
- /** Determine which providers are configured (priority: Exa → Brave → Jina → Perplexity → Anthropic → Gemini → Codex → Z.AI → Synthetic) */
55
+ /** Determine which providers are configured (priority: Perplexity → Exa → Brave → Jina → Kimi → Anthropic → Gemini → Codex → Z.AI → Synthetic) */
53
56
  export async function resolveProviderChain(
54
57
  preferredProvider: SearchProviderId | "auto" = preferredProvId,
55
58
  ): Promise<SearchProvider[]> {
@@ -0,0 +1,196 @@
1
+ /**
2
+ * Kimi Web Search Provider
3
+ *
4
+ * Uses Moonshot Kimi Code search API to retrieve web results.
5
+ * Endpoint: POST https://api.kimi.com/coding/v1/search
6
+ */
7
+ import { getEnvApiKey } from "@oh-my-pi/pi-ai";
8
+ import { $env } from "@oh-my-pi/pi-utils";
9
+ import { getAgentDbPath } from "@oh-my-pi/pi-utils/dirs";
10
+ import { AgentStorage } from "../../../session/agent-storage";
11
+ import type { SearchResponse, SearchSource } from "../../../web/search/types";
12
+ import { SearchProviderError } from "../../../web/search/types";
13
+ import type { SearchParams } from "./base";
14
+ import { SearchProvider } from "./base";
15
+
16
+ const KIMI_SEARCH_URL = "https://api.kimi.com/coding/v1/search";
17
+
18
+ const DEFAULT_NUM_RESULTS = 10;
19
+ const MAX_NUM_RESULTS = 20;
20
+ const DEFAULT_TIMEOUT_SECONDS = 30;
21
+
22
+ export interface KimiSearchParams {
23
+ query: string;
24
+ num_results?: number;
25
+ include_content?: boolean;
26
+ signal?: AbortSignal;
27
+ }
28
+
29
+ interface KimiSearchResult {
30
+ site_name?: string;
31
+ title?: string;
32
+ url?: string;
33
+ snippet?: string;
34
+ content?: string;
35
+ date?: string;
36
+ icon?: string;
37
+ mime?: string;
38
+ }
39
+
40
+ interface KimiSearchResponse {
41
+ search_results?: KimiSearchResult[];
42
+ }
43
+
44
+ function asTrimmed(value: string | undefined): string | undefined {
45
+ if (!value) return undefined;
46
+ const trimmed = value.trim();
47
+ return trimmed.length > 0 ? trimmed : undefined;
48
+ }
49
+
50
+ function resolveBaseUrl(): string {
51
+ return asTrimmed($env.MOONSHOT_SEARCH_BASE_URL) ?? asTrimmed($env.KIMI_SEARCH_BASE_URL) ?? KIMI_SEARCH_URL;
52
+ }
53
+
54
+ function clampNumResults(value: number | undefined): number {
55
+ if (!value || Number.isNaN(value)) return DEFAULT_NUM_RESULTS;
56
+ return Math.min(MAX_NUM_RESULTS, Math.max(1, value));
57
+ }
58
+
59
+ function dateToAgeSeconds(dateStr: string | undefined): number | undefined {
60
+ if (!dateStr) return undefined;
61
+ try {
62
+ const date = new Date(dateStr);
63
+ if (Number.isNaN(date.getTime())) return undefined;
64
+ return Math.floor((Date.now() - date.getTime()) / 1000);
65
+ } catch {
66
+ return undefined;
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Find Kimi search credentials from environment or agent.db credentials.
72
+ * Priority: MOONSHOT_SEARCH_API_KEY / KIMI_SEARCH_API_KEY / MOONSHOT_API_KEY, then agent.db providers "moonshot" or "kimi-code".
73
+ */
74
+ async function findApiKey(): Promise<string | null> {
75
+ const envKey =
76
+ asTrimmed($env.MOONSHOT_SEARCH_API_KEY) ??
77
+ asTrimmed($env.KIMI_SEARCH_API_KEY) ??
78
+ getEnvApiKey("moonshot") ??
79
+ null;
80
+ if (envKey) return envKey;
81
+
82
+ try {
83
+ const storage = await AgentStorage.open(getAgentDbPath());
84
+ for (const provider of ["moonshot", "kimi-code"] as const) {
85
+ const records = storage.listAuthCredentials(provider);
86
+ for (const record of records) {
87
+ const credential = record.credential;
88
+ if (credential.type === "api_key" && credential.key.trim().length > 0) {
89
+ return credential.key;
90
+ }
91
+ if (credential.type === "oauth" && credential.access.trim().length > 0) {
92
+ return credential.access;
93
+ }
94
+ }
95
+ }
96
+ } catch {
97
+ return null;
98
+ }
99
+
100
+ return null;
101
+ }
102
+
103
+ async function callKimiSearch(
104
+ apiKey: string,
105
+ params: { query: string; limit: number; includeContent: boolean; signal?: AbortSignal },
106
+ ): Promise<{ response: KimiSearchResponse; requestId?: string }> {
107
+ const response = await fetch(resolveBaseUrl(), {
108
+ method: "POST",
109
+ headers: {
110
+ Accept: "application/json",
111
+ "Content-Type": "application/json",
112
+ Authorization: `Bearer ${apiKey}`,
113
+ },
114
+ body: JSON.stringify({
115
+ text_query: params.query,
116
+ limit: params.limit,
117
+ enable_page_crawling: params.includeContent,
118
+ timeout_seconds: DEFAULT_TIMEOUT_SECONDS,
119
+ }),
120
+ signal: params.signal,
121
+ });
122
+
123
+ if (!response.ok) {
124
+ const errorText = await response.text();
125
+ throw new SearchProviderError(
126
+ "kimi",
127
+ `Kimi search API error (${response.status}): ${errorText}`,
128
+ response.status,
129
+ );
130
+ }
131
+
132
+ const data = (await response.json()) as KimiSearchResponse;
133
+ const requestId = response.headers.get("x-request-id") ?? response.headers.get("x-msh-request-id") ?? undefined;
134
+ return { response: data, requestId };
135
+ }
136
+
137
+ /** Execute Kimi web search. */
138
+ export async function searchKimi(params: KimiSearchParams): Promise<SearchResponse> {
139
+ const apiKey = await findApiKey();
140
+ if (!apiKey) {
141
+ throw new Error(
142
+ "Kimi search credentials not found. Set MOONSHOT_SEARCH_API_KEY, KIMI_SEARCH_API_KEY, MOONSHOT_API_KEY, or login with 'omp /login moonshot'.",
143
+ );
144
+ }
145
+
146
+ const limit = clampNumResults(params.num_results);
147
+ const { response, requestId } = await callKimiSearch(apiKey, {
148
+ query: params.query,
149
+ limit,
150
+ includeContent: params.include_content ?? false,
151
+ signal: params.signal,
152
+ });
153
+ const sources: SearchSource[] = [];
154
+
155
+ for (const result of response.search_results ?? []) {
156
+ if (!result.url) continue;
157
+ const publishedDate = asTrimmed(result.date);
158
+ const snippet = asTrimmed(result.snippet) ?? asTrimmed(result.content);
159
+ sources.push({
160
+ title: asTrimmed(result.title) ?? result.url,
161
+ url: result.url,
162
+ snippet,
163
+ publishedDate,
164
+ ageSeconds: dateToAgeSeconds(publishedDate),
165
+ author: asTrimmed(result.site_name),
166
+ });
167
+ }
168
+
169
+ return {
170
+ provider: "kimi",
171
+ sources: sources.slice(0, limit),
172
+ requestId,
173
+ };
174
+ }
175
+
176
+ /** Search provider for Kimi web search. */
177
+ export class KimiProvider extends SearchProvider {
178
+ readonly id = "kimi";
179
+ readonly label = "Kimi";
180
+
181
+ async isAvailable(): Promise<boolean> {
182
+ try {
183
+ return !!(await findApiKey());
184
+ } catch {
185
+ return false;
186
+ }
187
+ }
188
+
189
+ search(params: SearchParams): Promise<SearchResponse> {
190
+ return searchKimi({
191
+ query: params.query,
192
+ num_results: params.numSearchResults ?? params.limit,
193
+ signal: params.signal,
194
+ });
195
+ }
196
+ }
@@ -9,6 +9,7 @@ export type SearchProviderId =
9
9
  | "exa"
10
10
  | "brave"
11
11
  | "jina"
12
+ | "kimi"
12
13
  | "zai"
13
14
  | "anthropic"
14
15
  | "perplexity"