@oh-my-pi/pi-coding-agent 12.11.2 → 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.
@@ -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"