pi-provider-kimi-code 0.5.0 → 0.6.0

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.
@@ -0,0 +1,191 @@
1
+ import { defineTool } from "@earendil-works/pi-coding-agent";
2
+ import { Type, type Static } from "@earendil-works/pi-ai";
3
+
4
+ import {
5
+ type BuildKimiToolOptions,
6
+ buildHeaders,
7
+ getKimiDatasourceUrl,
8
+ buildKimiToolDeps,
9
+ buildTimeoutSignal,
10
+ errorResult,
11
+ fetchWithAuthRetry,
12
+ firstText,
13
+ readErrorBody,
14
+ shouldCollapse,
15
+ textComponent,
16
+ } from "./common.ts";
17
+
18
+ const DATASOURCE_DEFINITIONS = [
19
+ {
20
+ name: "stock_finance_data",
21
+ description: "A/HK/US stocks, prices, financial reports, indicators, shareholders.",
22
+ },
23
+ {
24
+ name: "yahoo_finance",
25
+ description: "Global finance: quotes, analyst ratings, options, indexes, historical prices.",
26
+ },
27
+ {
28
+ name: "world_bank_open_data",
29
+ description: "World Bank macro data: GDP, inflation, population, country comparisons.",
30
+ },
31
+ {
32
+ name: "tianyancha",
33
+ description: "Chinese enterprise registry: shareholders, legal representative, risks, patents.",
34
+ },
35
+ { name: "arxiv", description: "arXiv preprints: paper search, abstracts, arXiv IDs." },
36
+ {
37
+ name: "scholar",
38
+ description: "Google Scholar: academic papers, authors, citations, high-citation surveys.",
39
+ },
40
+ {
41
+ name: "yuandian_law",
42
+ description: "Chinese laws, regulations, judicial cases; labor contracts and legal clauses.",
43
+ },
44
+ ] as const;
45
+
46
+ const DATASOURCE_NAMES = DATASOURCE_DEFINITIONS.map((source) => source.name);
47
+
48
+ export const kimiDatasourceSchema = Type.Object({
49
+ data_source_name: Type.Union(
50
+ DATASOURCE_NAMES.map((name) => Type.Literal(name)),
51
+ {
52
+ description:
53
+ "Source: stock_finance_data stocks/reports; yahoo_finance quotes/options; world_bank_open_data GDP/population; tianyancha Chinese company registry/legal rep; arxiv preprints; scholar citations; yuandian_law Chinese laws/cases/contracts.",
54
+ },
55
+ ),
56
+ api_name: Type.Optional(
57
+ Type.String({
58
+ description: "API name from the datasource docs. Omit to get docs first.",
59
+ }),
60
+ ),
61
+ params: Type.Optional(
62
+ Type.Record(Type.String(), Type.Unknown(), {
63
+ description: "Parameters matching the selected datasource API docs.",
64
+ }),
65
+ ),
66
+ });
67
+
68
+ export type KimiDatasourceParams = Static<typeof kimiDatasourceSchema>;
69
+
70
+ interface DatasourceToolResponse {
71
+ is_success?: boolean;
72
+ error?: unknown;
73
+ result?: {
74
+ assistant?: unknown;
75
+ user?: unknown;
76
+ };
77
+ }
78
+
79
+ function extractTextFromResult(result: unknown): string {
80
+ if (!Array.isArray(result)) return "";
81
+ const texts: string[] = [];
82
+ for (const item of result) {
83
+ if (typeof item === "string") {
84
+ texts.push(item);
85
+ } else if (typeof item === "object" && item !== null) {
86
+ const record = item as Record<string, unknown>;
87
+ if (typeof record.text === "string") {
88
+ texts.push(record.text);
89
+ }
90
+ }
91
+ }
92
+ return texts.join("\n");
93
+ }
94
+
95
+ function extractResponseText(data: DatasourceToolResponse): string {
96
+ if (data.result?.assistant) {
97
+ const text = extractTextFromResult(data.result.assistant);
98
+ if (text) return text;
99
+ }
100
+ if (data.result?.user) {
101
+ const text = extractTextFromResult(data.result.user);
102
+ if (text) return text;
103
+ }
104
+ return JSON.stringify(data, null, 2);
105
+ }
106
+
107
+ export function buildKimiDatasourceTool(options: BuildKimiToolOptions = {}) {
108
+ const deps = buildKimiToolDeps(options);
109
+ const defaultCollapsed = options.defaultCollapsed ?? true;
110
+
111
+ return defineTool({
112
+ name: "kimi_datasource",
113
+ label: "Kimi Datasource",
114
+ description:
115
+ "Call Kimi datasource APIs. Covers stocks/finance, macro data, Chinese company registry, papers, and Chinese law/cases/contracts. First omit api_name to get docs, then call api_name with documented params.",
116
+ promptSnippet: "Query Kimi datasource APIs; get docs first, then call documented api_name",
117
+ parameters: kimiDatasourceSchema,
118
+
119
+ async execute(toolCallId, params, signal) {
120
+ const accessToken = deps.getAccessToken();
121
+ if (!accessToken) {
122
+ return errorResult<string>(
123
+ "Missing Kimi Code OAuth credentials. Run /login kimi-coding first.",
124
+ );
125
+ }
126
+
127
+ const timeout = buildTimeoutSignal(signal);
128
+ try {
129
+ const apiName = params.api_name?.trim();
130
+ const body = apiName
131
+ ? {
132
+ method: "call_data_source_tool",
133
+ params: {
134
+ data_source_name: params.data_source_name,
135
+ api_name: apiName,
136
+ params: params.params ?? {},
137
+ },
138
+ }
139
+ : {
140
+ method: "get_data_source_desc",
141
+ params: { data_source_name: params.data_source_name },
142
+ };
143
+
144
+ const response = await fetchWithAuthRetry(deps, accessToken, (token) =>
145
+ deps.fetch(getKimiDatasourceUrl(), {
146
+ method: "POST",
147
+ headers: buildHeaders(token, toolCallId),
148
+ body: JSON.stringify(body),
149
+ signal: timeout.signal,
150
+ }),
151
+ );
152
+
153
+ if (!response.ok) {
154
+ const bodyText = await readErrorBody(response);
155
+ return errorResult<string>(
156
+ `kimi_datasource failed: ${response.status}${bodyText ? ` ${bodyText}` : ""}`,
157
+ );
158
+ }
159
+
160
+ const data = (await response.json()) as DatasourceToolResponse;
161
+ if (data.is_success === false) {
162
+ const errorText =
163
+ typeof data.error === "string"
164
+ ? data.error
165
+ : JSON.stringify(data.error ?? data, null, 2);
166
+ return errorResult<string>(`kimi_datasource error: ${errorText}`);
167
+ }
168
+
169
+ const text = extractResponseText(data);
170
+ return {
171
+ content: [{ type: "text", text }],
172
+ details: text,
173
+ };
174
+ } catch (error) {
175
+ const message = error instanceof Error ? error.message : String(error);
176
+ return errorResult<string>(`kimi_datasource failed: ${message}`);
177
+ } finally {
178
+ timeout.cleanup();
179
+ }
180
+ },
181
+
182
+ renderResult(result, renderOptions) {
183
+ const text = firstText(result);
184
+ if (!shouldCollapse(defaultCollapsed, renderOptions.expanded)) {
185
+ return textComponent(text);
186
+ }
187
+ const preview = text.length > 200 ? `${text.slice(0, 200)}…` : text;
188
+ return textComponent(`kimi_datasource: ${text.length} chars\n${preview}`);
189
+ },
190
+ });
191
+ }
@@ -1,11 +1,19 @@
1
- import { AuthStorage, defineTool, type AgentToolResult } from "@earendil-works/pi-coding-agent";
1
+ import { defineTool, type AgentToolResult } from "@earendil-works/pi-coding-agent";
2
2
  import { Type, type Static } from "@earendil-works/pi-ai";
3
3
 
4
- import { PROVIDER_ID } from "../constants.ts";
5
- import { getCommonHeaders } from "../device.ts";
6
-
7
- const MOONSHOT_BASE_V1 = "https://api.kimi.com/coding/v1";
8
- const MOONSHOT_TIMEOUT_MS = 180_000;
4
+ import {
5
+ type BuildKimiToolOptions,
6
+ buildHeaders,
7
+ getKimiBaseV1,
8
+ buildKimiToolDeps,
9
+ buildTimeoutSignal,
10
+ errorResult,
11
+ fetchWithAuthRetry,
12
+ firstText,
13
+ readErrorBody,
14
+ shouldCollapse,
15
+ textComponent,
16
+ } from "./common.ts";
9
17
 
10
18
  export const moonshotSearchSchema = Type.Object({
11
19
  query: Type.String({ description: "The query text to search for." }),
@@ -44,74 +52,13 @@ export interface MoonshotFetchResult {
44
52
  content: string;
45
53
  }
46
54
 
47
- interface MoonshotToolDeps {
48
- fetch: typeof fetch;
49
- getAccessToken: () => string | null;
50
- }
51
-
52
- export interface BuildMoonshotToolOptions {
53
- deps?: Partial<MoonshotToolDeps>;
54
- defaultCollapsed?: boolean;
55
- }
56
-
57
- interface MoonshotSearchResponse {
58
- search_results?: unknown;
59
- }
60
-
61
- function defaultGetAccessToken(): string | null {
62
- const credential = AuthStorage.create().get(PROVIDER_ID);
63
- if (!credential || credential.type !== "oauth") return null;
64
- return credential.access;
65
- }
66
-
67
- function buildDeps(options: BuildMoonshotToolOptions = {}): MoonshotToolDeps {
68
- return {
69
- fetch: options.deps?.fetch ?? fetch,
70
- getAccessToken: options.deps?.getAccessToken ?? defaultGetAccessToken,
71
- };
72
- }
73
-
74
55
  function clampLimit(value: number | undefined): number {
75
56
  if (typeof value !== "number" || !Number.isFinite(value)) return 5;
76
57
  return Math.max(1, Math.min(20, Math.trunc(value)));
77
58
  }
78
59
 
79
- function errorResult<T>(message: string): AgentToolResult<T> {
80
- return {
81
- content: [{ type: "text", text: message }],
82
- details: undefined as T,
83
- };
84
- }
85
-
86
- function buildTimeoutSignal(signal: AbortSignal | undefined): {
87
- signal: AbortSignal;
88
- cleanup: () => void;
89
- } {
90
- const controller = new AbortController();
91
- const timeout = setTimeout(() => controller.abort(), MOONSHOT_TIMEOUT_MS);
92
- const abort = () => controller.abort();
93
- if (signal?.aborted) {
94
- controller.abort();
95
- } else {
96
- signal?.addEventListener("abort", abort, { once: true });
97
- }
98
-
99
- return {
100
- signal: controller.signal,
101
- cleanup: () => {
102
- clearTimeout(timeout);
103
- signal?.removeEventListener("abort", abort);
104
- },
105
- };
106
- }
107
-
108
- function buildHeaders(accessToken: string, toolCallId: string): Record<string, string> {
109
- return {
110
- ...getCommonHeaders(),
111
- Authorization: `Bearer ${accessToken}`,
112
- "Content-Type": "application/json",
113
- "X-Msh-Tool-Call-Id": toolCallId,
114
- };
60
+ interface MoonshotSearchResponse {
61
+ search_results?: unknown;
115
62
  }
116
63
 
117
64
  function stringField(record: Record<string, unknown>, key: string): string {
@@ -141,22 +88,6 @@ function searchResultsText(results: MoonshotSearchResult[]): string {
141
88
  return JSON.stringify(results, null, 2);
142
89
  }
143
90
 
144
- function textComponent(text: string) {
145
- return {
146
- render: () => text.split("\n"),
147
- invalidate: () => {},
148
- };
149
- }
150
-
151
- function firstText(result: AgentToolResult<unknown>): string {
152
- const first = result.content[0];
153
- return first?.type === "text" ? first.text : "";
154
- }
155
-
156
- function shouldCollapse(defaultCollapsed: boolean, expanded: boolean | undefined): boolean {
157
- return defaultCollapsed && expanded !== true;
158
- }
159
-
160
91
  function renderSearchResult(
161
92
  result: AgentToolResult<MoonshotSearchResult[]>,
162
93
  expanded: boolean | undefined,
@@ -192,12 +123,8 @@ function renderFetchResult(
192
123
  );
193
124
  }
194
125
 
195
- async function readErrorBody(response: Response): Promise<string> {
196
- return response.text().catch(() => "");
197
- }
198
-
199
- export function buildMoonshotSearchTool(options: BuildMoonshotToolOptions = {}) {
200
- const deps = buildDeps(options);
126
+ export function buildMoonshotSearchTool(options: BuildKimiToolOptions = {}) {
127
+ const deps = buildKimiToolDeps(options);
201
128
  const defaultCollapsed = options.defaultCollapsed ?? true;
202
129
 
203
130
  return defineTool({
@@ -219,17 +146,19 @@ export function buildMoonshotSearchTool(options: BuildMoonshotToolOptions = {})
219
146
  const includeContent = params.include_content === true;
220
147
  const timeout = buildTimeoutSignal(signal);
221
148
  try {
222
- const response = await deps.fetch(`${MOONSHOT_BASE_V1}/search`, {
223
- method: "POST",
224
- headers: buildHeaders(accessToken, toolCallId),
225
- body: JSON.stringify({
226
- text_query: params.query,
227
- limit,
228
- enable_page_crawling: includeContent,
229
- timeout_seconds: 30,
149
+ const response = await fetchWithAuthRetry(deps, accessToken, (token) =>
150
+ deps.fetch(`${getKimiBaseV1()}/search`, {
151
+ method: "POST",
152
+ headers: buildHeaders(token, toolCallId),
153
+ body: JSON.stringify({
154
+ text_query: params.query,
155
+ limit,
156
+ enable_page_crawling: includeContent,
157
+ timeout_seconds: 30,
158
+ }),
159
+ signal: timeout.signal,
230
160
  }),
231
- signal: timeout.signal,
232
- });
161
+ );
233
162
 
234
163
  if (!response.ok) {
235
164
  const body = await readErrorBody(response);
@@ -258,8 +187,8 @@ export function buildMoonshotSearchTool(options: BuildMoonshotToolOptions = {})
258
187
  });
259
188
  }
260
189
 
261
- export function buildMoonshotFetchTool(options: BuildMoonshotToolOptions = {}) {
262
- const deps = buildDeps(options);
190
+ export function buildMoonshotFetchTool(options: BuildKimiToolOptions = {}) {
191
+ const deps = buildKimiToolDeps(options);
263
192
  const defaultCollapsed = options.defaultCollapsed ?? true;
264
193
 
265
194
  return defineTool({
@@ -279,15 +208,17 @@ export function buildMoonshotFetchTool(options: BuildMoonshotToolOptions = {}) {
279
208
 
280
209
  const timeout = buildTimeoutSignal(signal);
281
210
  try {
282
- const response = await deps.fetch(`${MOONSHOT_BASE_V1}/fetch`, {
283
- method: "POST",
284
- headers: {
285
- ...buildHeaders(accessToken, toolCallId),
286
- Accept: "text/markdown",
287
- },
288
- body: JSON.stringify({ url: params.url }),
289
- signal: timeout.signal,
290
- });
211
+ const response = await fetchWithAuthRetry(deps, accessToken, (token) =>
212
+ deps.fetch(`${getKimiBaseV1()}/fetch`, {
213
+ method: "POST",
214
+ headers: {
215
+ ...buildHeaders(token, toolCallId),
216
+ Accept: "text/markdown",
217
+ },
218
+ body: JSON.stringify({ url: params.url }),
219
+ signal: timeout.signal,
220
+ }),
221
+ );
291
222
 
292
223
  if (!response.ok) {
293
224
  const body = await readErrorBody(response);