pi-provider-kimi-code 0.3.2 → 0.4.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.
- package/README.md +84 -45
- package/index.ts +350 -1
- package/package.json +1 -1
- package/src/config.ts +151 -0
- package/src/tools/moonshot.ts +316 -0
package/README.md
CHANGED
|
@@ -1,42 +1,33 @@
|
|
|
1
1
|
# Pi extension for Kimi Code
|
|
2
|
+
|
|
2
3
|
[](https://www.npmjs.com/package/pi-provider-kimi-code)
|
|
3
4
|
[](./LICENSE)
|
|
4
5
|
|
|
5
|
-
**Use Kimi Code in Pi
|
|
6
|
+
**Use Kimi Code in Pi, with the Kimi parts handled.**
|
|
6
7
|
|
|
7
|
-
Pi
|
|
8
|
+
Pi already has a basic `kimi-coding` provider. This extension is for the parts that start to matter once Kimi Code becomes part of your real Pi workflow: account login reuse, file uploads, measured cache behavior, live model metadata, and optional Moonshot search/fetch tools.
|
|
8
9
|
|
|
9
10
|
## Why this exists
|
|
10
11
|
|
|
11
|
-
Pi
|
|
12
|
-
|
|
13
|
-
This extension is for the next layer. If you are building real Pi workflows, the Kimi-specific details start to matter:
|
|
14
|
-
|
|
15
|
-
- **Files should go through Kimi's Files API.** Large images are uploaded to `/files` and referenced as `ms://` IDs instead of being sent as huge inline base64 payloads.
|
|
16
|
-
- **Cache behavior should be visible.** Kimi caches by content prefix, not by `prompt_cache_key`. This repo measures that behavior and keeps the provider aligned with it.
|
|
17
|
-
- **Existing Kimi sessions should carry over.** If you already use `kimi-cli`, this extension can reuse that login instead of making you manage another API key.
|
|
18
|
-
- **Protocol choice should stay available.** The default is OpenAI-compatible mode, but Anthropic-compatible mode is still there when your Pi setup needs it.
|
|
19
|
-
|
|
20
|
-
> On prompt caching: the built-in provider works fine. Kimi's Coding endpoint caches by content prefix hash automatically — neither `cache_control` markers nor `prompt_cache_key` actually drive cache hits. See [docs/caching.md](docs/caching.md) for measurements.
|
|
21
|
-
|
|
22
|
-
## Who is this for?
|
|
12
|
+
Pi is a small harness you adapt to your own workflow. Kimi CLI is Moonshot's official Kimi Code path. This package sits between them: Kimi Code as a Pi provider, plus the Kimi-specific details that the basic provider keeps simple.
|
|
23
13
|
|
|
24
|
-
-
|
|
25
|
-
-
|
|
26
|
-
-
|
|
27
|
-
|
|
28
|
-
|
|
14
|
+
- **Use your Kimi account.** Log in with `/login kimi-coding`, or reuse an existing `kimi-cli` session.
|
|
15
|
+
- **Send files the Kimi way.** Large inline images go through Kimi's Files API and become `ms://` references instead of huge base64 payloads.
|
|
16
|
+
- **Know what the cache is doing.** Kimi caches by content prefix. This repo measures that behavior instead of pretending `prompt_cache_key` controls it.
|
|
17
|
+
- **Use the safer protocol by default.** OpenAI-compatible mode is the default because tool results are less ambiguous there. Anthropic-compatible mode is still available.
|
|
18
|
+
- **Turn on Kimi-native tools when you want them.** `moonshot_search` and `moonshot_fetch` are opt-in, configurable per user or per project.
|
|
29
19
|
|
|
30
20
|
## What this package adds
|
|
31
21
|
|
|
32
|
-
-
|
|
33
|
-
-
|
|
34
|
-
-
|
|
35
|
-
-
|
|
36
|
-
-
|
|
37
|
-
-
|
|
38
|
-
-
|
|
39
|
-
-
|
|
22
|
+
- Kimi account login in Pi, plus `KIMI_API_KEY` for CI or pay-per-token use.
|
|
23
|
+
- `kimi-cli` credential reuse from `~/.kimi/credentials/kimi-code.json`.
|
|
24
|
+
- Kimi Files API uploads for large inline images.
|
|
25
|
+
- Live model metadata from Kimi at login / refresh time.
|
|
26
|
+
- OpenAI-compatible mode by default, Anthropic-compatible mode on request.
|
|
27
|
+
- Kimi K2.6 reasoning level mapping for Pi's reasoning controls.
|
|
28
|
+
- Stream cleanup for Kimi's thinking-only placeholder text.
|
|
29
|
+
- Optional `moonshot_search` and `moonshot_fetch` tools via `/kimi-settings`.
|
|
30
|
+
- No build step; Pi loads the TypeScript extension directly.
|
|
40
31
|
|
|
41
32
|
## Install
|
|
42
33
|
|
|
@@ -57,32 +48,29 @@ pi -e /path/to/pi-provider-kimi-code
|
|
|
57
48
|
If you prefer not to use npm, download the tarball from the [latest release](https://github.com/Leechael/pi-provider-kimi-code/releases/latest), extract it, and install from the local path:
|
|
58
49
|
|
|
59
50
|
```bash
|
|
60
|
-
# Download and extract
|
|
61
51
|
curl -L https://github.com/Leechael/pi-provider-kimi-code/releases/latest/download/pi-provider-kimi-code.tar.gz | tar -xz -C /tmp
|
|
62
|
-
|
|
63
|
-
# Install from the extracted directory
|
|
64
52
|
pi install /tmp/pi-provider-kimi-code
|
|
65
53
|
```
|
|
66
54
|
|
|
67
55
|
## Sign in
|
|
68
56
|
|
|
69
|
-
### Browser login
|
|
70
|
-
|
|
71
57
|
Inside Pi, run:
|
|
72
58
|
|
|
73
|
-
```
|
|
59
|
+
```text
|
|
74
60
|
/login kimi-coding
|
|
75
61
|
```
|
|
76
62
|
|
|
77
|
-
A browser tab opens, you sign into your Kimi account, and
|
|
63
|
+
A browser tab opens, you sign into your Kimi account, and Pi stores the credential at `~/.pi/agent/auth.json`. Tokens refresh automatically.
|
|
78
64
|
|
|
79
|
-
|
|
65
|
+
If you already use `kimi-cli`, this extension can reuse its credential file:
|
|
80
66
|
|
|
81
|
-
|
|
67
|
+
```text
|
|
68
|
+
~/.kimi/credentials/kimi-code.json
|
|
69
|
+
```
|
|
82
70
|
|
|
83
|
-
|
|
71
|
+
The file is read-only from this extension's side. Set `KIMI_SHARE_DIR` if your `kimi-cli` share directory lives somewhere else.
|
|
84
72
|
|
|
85
|
-
For CI or pay-per-token use, set `KIMI_API_KEY
|
|
73
|
+
For CI or pay-per-token use, set `KIMI_API_KEY` instead:
|
|
86
74
|
|
|
87
75
|
```bash
|
|
88
76
|
KIMI_API_KEY=sk-... pi
|
|
@@ -92,13 +80,19 @@ KIMI_API_KEY=sk-... pi
|
|
|
92
80
|
|
|
93
81
|
This provider publishes one Pi model ID:
|
|
94
82
|
|
|
95
|
-
```
|
|
83
|
+
```text
|
|
96
84
|
kimi-coding/kimi-for-coding
|
|
97
85
|
```
|
|
98
86
|
|
|
99
|
-
|
|
87
|
+
Select it inside Pi:
|
|
88
|
+
|
|
89
|
+
```text
|
|
90
|
+
/model kimi-coding/kimi-for-coding
|
|
91
|
+
```
|
|
100
92
|
|
|
101
|
-
|
|
93
|
+
Kimi keeps Coding models behind aliases. Rather than hardcoding a stale model list, this extension asks Kimi for the current model info when you log in or refresh. If your account is on a newer rollout or internal test, Pi can pick up the latest model name and context size without waiting for a package release.
|
|
94
|
+
|
|
95
|
+
Fallback values:
|
|
102
96
|
|
|
103
97
|
- Context window: `262144` tokens
|
|
104
98
|
- Max output: `32000` tokens
|
|
@@ -114,12 +108,45 @@ Kimi K2.6 supports Pi's reasoning levels. This provider maps them to Kimi's curr
|
|
|
114
108
|
|
|
115
109
|
If Kimi reports a larger context window, the provider uses that value. You can also override the advertised context or model name with `KIMI_MODEL_MAX_CONTEXT_SIZE` / `KIMI_MODEL_NAME`; see [docs/ENV.md](docs/ENV.md).
|
|
116
110
|
|
|
117
|
-
|
|
111
|
+
## Optional Moonshot tools
|
|
112
|
+
|
|
113
|
+
Kimi Coding has server-side search and fetch tools. This extension can expose them to Pi as:
|
|
114
|
+
|
|
115
|
+
- `moonshot_search`
|
|
116
|
+
- `moonshot_fetch`
|
|
118
117
|
|
|
118
|
+
They are off by default. Turn them on when you want Kimi's own search/fetch path in the agent loop.
|
|
119
|
+
|
|
120
|
+
Inside Pi, run:
|
|
121
|
+
|
|
122
|
+
```text
|
|
123
|
+
/kimi-settings
|
|
119
124
|
```
|
|
120
|
-
|
|
125
|
+
|
|
126
|
+
That command shows your current Kimi usage summary and lets you edit the home or project config. Changes apply to the active session tool set.
|
|
127
|
+
|
|
128
|
+
Config files are JSON:
|
|
129
|
+
|
|
130
|
+
- Home: `~/.pi/pi-provider-kimi-code.json`
|
|
131
|
+
- Project override: `<cwd>/.pi/pi-provider-kimi-code.json`
|
|
132
|
+
|
|
133
|
+
Project config overrides home config with a deep merge. Missing files or missing keys mean both tools stay off.
|
|
134
|
+
|
|
135
|
+
```json
|
|
136
|
+
{
|
|
137
|
+
"tools": {
|
|
138
|
+
"moonshot_search": { "enabled": true, "default_collapsed": true },
|
|
139
|
+
"moonshot_fetch": { "enabled": true, "default_collapsed": true }
|
|
140
|
+
}
|
|
141
|
+
}
|
|
121
142
|
```
|
|
122
143
|
|
|
144
|
+
Both tools require `/login kimi-coding` OAuth credentials. `KIMI_API_KEY` is not used for these tools. Your account also needs access to Moonshot's server-side search/fetch services; if not, the upstream service can return subscription or whitelist errors.
|
|
145
|
+
|
|
146
|
+
`default_collapsed` controls only the TUI preview. The full tool result still goes to the model; set it to `false` if you want previews expanded by default.
|
|
147
|
+
|
|
148
|
+
If you already use another search or fetch tool, pick one path for a session. Overlapping tools can make the model choose the wrong one.
|
|
149
|
+
|
|
123
150
|
## Common knobs
|
|
124
151
|
|
|
125
152
|
Most users do not need environment variables. Two are worth knowing:
|
|
@@ -127,7 +154,9 @@ Most users do not need environment variables. Two are worth knowing:
|
|
|
127
154
|
- `KIMI_API_KEY` — static API key for CI or pay-per-token use.
|
|
128
155
|
- `KIMI_CODE_PROTOCOL` — `openai` by default; set to `anthropic` if your Pi setup needs Anthropic-compatible requests.
|
|
129
156
|
|
|
130
|
-
|
|
157
|
+
Moonshot search/fetch tools are configured through `/kimi-settings` or JSON config files, not env vars.
|
|
158
|
+
|
|
159
|
+
The full env list, including base URL overrides, `kimi-cli` path overrides, upload tuning, debug logs, and model metadata overrides, lives in [docs/ENV.md](docs/ENV.md).
|
|
131
160
|
|
|
132
161
|
## Notes
|
|
133
162
|
|
|
@@ -170,12 +199,22 @@ Pi runs on Node's `fetch` / undici stack, which handles `http_proxy` / `https_pr
|
|
|
170
199
|
|
|
171
200
|
### `/login kimi-coding` prints a device code but the browser never opens
|
|
172
201
|
|
|
173
|
-
The login flow always prints a verification URL
|
|
202
|
+
The login flow always prints a verification URL. If your terminal or OS blocks auto-open, copy the URL and paste it into a browser manually.
|
|
174
203
|
|
|
175
204
|
### "Access denied" or subscription errors after a successful login
|
|
176
205
|
|
|
177
206
|
Your Moonshot account needs an active Kimi Code subscription for the provider to do anything useful. If the same account works in `kimi-cli`, re-run `/login kimi-coding` to refresh credentials.
|
|
178
207
|
|
|
208
|
+
### Moonshot tools do not show up
|
|
209
|
+
|
|
210
|
+
Run `/kimi-settings` and check whether `moonshot_search` / `moonshot_fetch` are enabled in the home or project config. These tools also require `/login kimi-coding`; `KIMI_API_KEY` is not enough.
|
|
211
|
+
|
|
212
|
+
If you changed the JSON config by hand, reload it through `/kimi-settings` or start a new Pi session.
|
|
213
|
+
|
|
214
|
+
### Moonshot tools return subscription or whitelist errors
|
|
215
|
+
|
|
216
|
+
The tools call Moonshot's server-side search/fetch services. Some accounts may not have access even with an active Kimi Code Plan. The error comes from the upstream service.
|
|
217
|
+
|
|
179
218
|
### Large images fail with a payload error
|
|
180
219
|
|
|
181
220
|
This extension uploads images over `KIMI_CODE_UPLOAD_THRESHOLD_BYTES` (default 1 MB) to Kimi's Files API and references them as `ms://`. Set `KIMI_CODE_DEBUG=1` to see upload decisions in the provider logs.
|
package/index.ts
CHANGED
|
@@ -19,8 +19,24 @@
|
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
22
|
-
import
|
|
22
|
+
import {
|
|
23
|
+
AuthStorage,
|
|
24
|
+
type ExtensionAPI,
|
|
25
|
+
type ExtensionCommandContext,
|
|
26
|
+
} from "@earendil-works/pi-coding-agent";
|
|
27
|
+
import os from "node:os";
|
|
28
|
+
import { relative } from "node:path";
|
|
23
29
|
|
|
30
|
+
import {
|
|
31
|
+
type KimiCodeConfig,
|
|
32
|
+
getProjectKimiCodeConfigPath,
|
|
33
|
+
getGlobalKimiCodeConfigPath,
|
|
34
|
+
loadHomeKimiCodeConfig,
|
|
35
|
+
loadKimiCodeConfig,
|
|
36
|
+
loadProjectKimiCodeConfig,
|
|
37
|
+
saveHomeKimiCodeConfig,
|
|
38
|
+
saveProjectKimiCodeConfig,
|
|
39
|
+
} from "./src/config.ts";
|
|
24
40
|
import {
|
|
25
41
|
DEFAULT_KIMI_MODEL_INPUT,
|
|
26
42
|
KIMI_API_TYPE,
|
|
@@ -35,8 +51,332 @@ import {
|
|
|
35
51
|
} from "./src/models.ts";
|
|
36
52
|
import { loginKimiCode, refreshKimiCodeToken } from "./src/oauth.ts";
|
|
37
53
|
import { streamSimpleKimi } from "./src/stream.ts";
|
|
54
|
+
import { buildMoonshotFetchTool, buildMoonshotSearchTool } from "./src/tools/moonshot.ts";
|
|
55
|
+
|
|
56
|
+
const MOONSHOT_TOOL_NAMES = ["moonshot_search", "moonshot_fetch"] as const;
|
|
57
|
+
type MoonshotToolName = (typeof MOONSHOT_TOOL_NAMES)[number];
|
|
58
|
+
type KimiConfigScope = "project" | "home";
|
|
59
|
+
const MEMBERSHIP_LEVEL_NAMES: Record<string, string> = {
|
|
60
|
+
LEVEL_FREE: "Free",
|
|
61
|
+
LEVEL_BASIC: "Adagio",
|
|
62
|
+
LEVEL_STANDARD: "Moderato",
|
|
63
|
+
LEVEL_INTERMEDIATE: "Allegretto",
|
|
64
|
+
LEVEL_ADVANCED: "Allegro",
|
|
65
|
+
LEVEL_PREMIUM: "Vivace",
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
interface UsageRow {
|
|
69
|
+
label: string;
|
|
70
|
+
used: number;
|
|
71
|
+
limit: number;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function registerConfiguredMoonshotTools(
|
|
75
|
+
pi: ExtensionAPI,
|
|
76
|
+
config: KimiCodeConfig,
|
|
77
|
+
options: { updateActiveTools: boolean },
|
|
78
|
+
): void {
|
|
79
|
+
if (config.tools.moonshot_search.enabled) {
|
|
80
|
+
pi.registerTool(
|
|
81
|
+
buildMoonshotSearchTool({
|
|
82
|
+
defaultCollapsed: config.tools.moonshot_search.default_collapsed,
|
|
83
|
+
}),
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
if (config.tools.moonshot_fetch.enabled) {
|
|
87
|
+
pi.registerTool(
|
|
88
|
+
buildMoonshotFetchTool({
|
|
89
|
+
defaultCollapsed: config.tools.moonshot_fetch.default_collapsed,
|
|
90
|
+
}),
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (!options.updateActiveTools) return;
|
|
95
|
+
|
|
96
|
+
const activeTools = new Set(pi.getActiveTools());
|
|
97
|
+
for (const toolName of MOONSHOT_TOOL_NAMES) {
|
|
98
|
+
if (config.tools[toolName].enabled) {
|
|
99
|
+
activeTools.add(toolName);
|
|
100
|
+
} else {
|
|
101
|
+
activeTools.delete(toolName);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
pi.setActiveTools([...activeTools]);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function getKimiUsageToken(): string | null {
|
|
108
|
+
const credential = AuthStorage.create().get(PROVIDER_ID);
|
|
109
|
+
if (credential?.type === "oauth" && credential.access) return credential.access;
|
|
110
|
+
const apiKey = process.env.KIMI_API_KEY?.trim();
|
|
111
|
+
return apiKey || null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function toNumber(value: unknown): number | null {
|
|
115
|
+
const number = Number(value);
|
|
116
|
+
return Number.isFinite(number) ? number : null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function parseUsageRow(value: unknown, fallbackLabel: string): UsageRow | null {
|
|
120
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
|
|
121
|
+
const record = value as Record<string, unknown>;
|
|
122
|
+
const limit = toNumber(record.limit);
|
|
123
|
+
const usedValue = toNumber(record.used);
|
|
124
|
+
const remaining = toNumber(record.remaining);
|
|
125
|
+
const used = usedValue ?? (limit !== null && remaining !== null ? limit - remaining : null);
|
|
126
|
+
if (limit === null && used === null) return null;
|
|
127
|
+
return {
|
|
128
|
+
label: String(record.name || record.title || fallbackLabel),
|
|
129
|
+
used: used ?? 0,
|
|
130
|
+
limit: limit ?? 0,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function parseUsageSummary(payload: unknown): string {
|
|
135
|
+
if (typeof payload !== "object" || payload === null || Array.isArray(payload)) {
|
|
136
|
+
return "Usage: unavailable";
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const record = payload as Record<string, unknown>;
|
|
140
|
+
const lines: string[] = [];
|
|
141
|
+
const membership = parseMembership(record);
|
|
142
|
+
if (membership) lines.push(membership);
|
|
143
|
+
|
|
144
|
+
const rows: UsageRow[] = [];
|
|
145
|
+
const summary = parseUsageRow(record.usage, "Weekly limit");
|
|
146
|
+
if (summary) rows.push(summary);
|
|
147
|
+
|
|
148
|
+
if (Array.isArray(record.limits)) {
|
|
149
|
+
for (const [index, item] of record.limits.entries()) {
|
|
150
|
+
const detail =
|
|
151
|
+
typeof item === "object" && item !== null && !Array.isArray(item)
|
|
152
|
+
? ((item as Record<string, unknown>).detail ?? item)
|
|
153
|
+
: item;
|
|
154
|
+
const row = parseUsageRow(detail, index === 0 ? "5h rate limit" : `Limit #${index + 1}`);
|
|
155
|
+
if (row) rows.push(row);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
lines.push(...rows.map(formatUsageRow));
|
|
160
|
+
return lines.length === 0 ? "Usage: no usage data" : lines.join("\n");
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function parseMembership(record: Record<string, unknown>): string | null {
|
|
164
|
+
const user = record.user;
|
|
165
|
+
if (typeof user !== "object" || user === null || Array.isArray(user)) return null;
|
|
166
|
+
const membership = (user as Record<string, unknown>).membership;
|
|
167
|
+
if (typeof membership !== "object" || membership === null || Array.isArray(membership)) {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
const level = (membership as Record<string, unknown>).level;
|
|
171
|
+
if (typeof level !== "string" || !level) return null;
|
|
172
|
+
const name = MEMBERSHIP_LEVEL_NAMES[level];
|
|
173
|
+
return name ? `Membership: ${name} (${level})` : `Membership: ${level}`;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function formatUsageRow(row: UsageRow): string {
|
|
177
|
+
if (row.limit <= 0) return `${row.label}: ${row.used} used`;
|
|
178
|
+
const remaining = Math.max(0, Math.min(row.limit - row.used, row.limit));
|
|
179
|
+
const percent = Math.round((remaining / row.limit) * 100);
|
|
180
|
+
return `${row.label}: ${quotaBar(remaining, row.limit)} ${percent}% left (${remaining}/${row.limit})`;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function quotaBar(remaining: number, limit: number): string {
|
|
184
|
+
const width = 20;
|
|
185
|
+
const filled = Math.max(0, Math.min(width, Math.round((remaining / limit) * width)));
|
|
186
|
+
return `[${"#".repeat(filled)}${"-".repeat(width - filled)}]`;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async function fetchKimiUsageSummary(): Promise<string> {
|
|
190
|
+
const token = getKimiUsageToken();
|
|
191
|
+
if (!token) return "Usage: missing credentials. Run /login kimi-coding.";
|
|
192
|
+
|
|
193
|
+
const controller = new AbortController();
|
|
194
|
+
const timeout = setTimeout(() => controller.abort(), 15_000);
|
|
195
|
+
try {
|
|
196
|
+
const response = await fetch("https://api.kimi.com/coding/v1/usages", {
|
|
197
|
+
method: "GET",
|
|
198
|
+
headers: {
|
|
199
|
+
...getCommonHeaders(),
|
|
200
|
+
Authorization: `Bearer ${token}`,
|
|
201
|
+
},
|
|
202
|
+
signal: controller.signal,
|
|
203
|
+
});
|
|
204
|
+
if (!response.ok) return `Usage: fetch failed (${response.status})`;
|
|
205
|
+
return parseUsageSummary(await response.json());
|
|
206
|
+
} catch (error) {
|
|
207
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
208
|
+
return `Usage: fetch failed (${message})`;
|
|
209
|
+
} finally {
|
|
210
|
+
clearTimeout(timeout);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function moonshotStatus(config: KimiCodeConfig): string {
|
|
215
|
+
return MOONSHOT_TOOL_NAMES.map((toolName) => {
|
|
216
|
+
const tool = config.tools[toolName];
|
|
217
|
+
const enabled = tool.enabled ? "enabled" : "disabled";
|
|
218
|
+
const collapsed = tool.default_collapsed ? "collapsed" : "expanded";
|
|
219
|
+
return `${toolName}: ${enabled}, default ${collapsed}`;
|
|
220
|
+
}).join("\n");
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function toggleEnabled(config: KimiCodeConfig, toolName: MoonshotToolName): KimiCodeConfig {
|
|
224
|
+
return {
|
|
225
|
+
tools: {
|
|
226
|
+
...config.tools,
|
|
227
|
+
[toolName]: {
|
|
228
|
+
...config.tools[toolName],
|
|
229
|
+
enabled: !config.tools[toolName].enabled,
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function toggleCollapsed(config: KimiCodeConfig, toolName: MoonshotToolName): KimiCodeConfig {
|
|
236
|
+
return {
|
|
237
|
+
tools: {
|
|
238
|
+
...config.tools,
|
|
239
|
+
[toolName]: {
|
|
240
|
+
...config.tools[toolName],
|
|
241
|
+
default_collapsed: !config.tools[toolName].default_collapsed,
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async function runKimiCommand(pi: ExtensionAPI, ctx: ExtensionCommandContext): Promise<void> {
|
|
248
|
+
let config = loadKimiCodeConfig({ cwd: ctx.cwd, home: os.homedir() });
|
|
249
|
+
let usage = await fetchKimiUsageSummary();
|
|
250
|
+
ctx.ui.notify(usage);
|
|
251
|
+
|
|
252
|
+
while (true) {
|
|
253
|
+
const choice = await ctx.ui.select(buildKimiMainTitle(config, ctx.cwd), [
|
|
254
|
+
`Edit project config (${relative(ctx.cwd, getProjectKimiCodeConfigPath(ctx.cwd))})`,
|
|
255
|
+
`Edit home config (${homeRelative(getGlobalKimiCodeConfigPath(os.homedir()))})`,
|
|
256
|
+
"Refresh usage",
|
|
257
|
+
"Done",
|
|
258
|
+
]);
|
|
259
|
+
|
|
260
|
+
if (!choice || choice === "Done") return;
|
|
261
|
+
if (choice === "Refresh usage") {
|
|
262
|
+
usage = await fetchKimiUsageSummary();
|
|
263
|
+
ctx.ui.notify(usage);
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
if (choice.startsWith("Edit project config")) {
|
|
267
|
+
config = await editConfigScope(pi, ctx, "project");
|
|
268
|
+
} else if (choice.startsWith("Edit home config")) {
|
|
269
|
+
config = await editConfigScope(pi, ctx, "home");
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async function editConfigScope(
|
|
275
|
+
pi: ExtensionAPI,
|
|
276
|
+
ctx: ExtensionCommandContext,
|
|
277
|
+
scope: KimiConfigScope,
|
|
278
|
+
): Promise<KimiCodeConfig> {
|
|
279
|
+
let current = loadScopeKimiCodeConfig(scope, ctx.cwd);
|
|
280
|
+
while (true) {
|
|
281
|
+
const choice = await ctx.ui.select(buildConfigScopeTitle(scope, current, ctx.cwd), [
|
|
282
|
+
toolMenuItem(current, "moonshot_search"),
|
|
283
|
+
toolMenuItem(current, "moonshot_fetch"),
|
|
284
|
+
"Back",
|
|
285
|
+
]);
|
|
286
|
+
if (!choice || choice === "Back") {
|
|
287
|
+
return loadKimiCodeConfig({ cwd: ctx.cwd, home: os.homedir() });
|
|
288
|
+
}
|
|
289
|
+
if (choice.startsWith("moonshot_search")) {
|
|
290
|
+
current = await editMoonshotTool(pi, ctx, scope, current, "moonshot_search");
|
|
291
|
+
} else if (choice.startsWith("moonshot_fetch")) {
|
|
292
|
+
current = await editMoonshotTool(pi, ctx, scope, current, "moonshot_fetch");
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
async function editMoonshotTool(
|
|
298
|
+
pi: ExtensionAPI,
|
|
299
|
+
ctx: ExtensionCommandContext,
|
|
300
|
+
scope: KimiConfigScope,
|
|
301
|
+
config: KimiCodeConfig,
|
|
302
|
+
toolName: MoonshotToolName,
|
|
303
|
+
): Promise<KimiCodeConfig> {
|
|
304
|
+
let current = config;
|
|
305
|
+
while (true) {
|
|
306
|
+
const tool = current.tools[toolName];
|
|
307
|
+
const choice = await ctx.ui.select(
|
|
308
|
+
`Edit ${toolName}\n\n${formatToolStatus(current, toolName)}`,
|
|
309
|
+
[
|
|
310
|
+
tool.enabled ? `Disable ${toolName}` : `Enable ${toolName}`,
|
|
311
|
+
tool.default_collapsed ? "Expand previews by default" : "Collapse previews by default",
|
|
312
|
+
"Back",
|
|
313
|
+
],
|
|
314
|
+
);
|
|
315
|
+
if (!choice || choice === "Back") return current;
|
|
316
|
+
if (choice.startsWith("Enable") || choice.startsWith("Disable")) {
|
|
317
|
+
current = toggleEnabled(current, toolName);
|
|
318
|
+
} else if (choice.endsWith("previews by default")) {
|
|
319
|
+
current = toggleCollapsed(current, toolName);
|
|
320
|
+
}
|
|
321
|
+
saveScopeKimiCodeConfig(scope, ctx.cwd, current);
|
|
322
|
+
const effective = loadKimiCodeConfig({ cwd: ctx.cwd, home: os.homedir() });
|
|
323
|
+
registerConfiguredMoonshotTools(pi, effective, { updateActiveTools: true });
|
|
324
|
+
ctx.ui.notify(`Saved ${toolName} config`, "info");
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function loadScopeKimiCodeConfig(scope: KimiConfigScope, cwd: string): KimiCodeConfig {
|
|
329
|
+
if (scope === "project") return loadProjectKimiCodeConfig(cwd);
|
|
330
|
+
return loadHomeKimiCodeConfig(os.homedir());
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function saveScopeKimiCodeConfig(
|
|
334
|
+
scope: KimiConfigScope,
|
|
335
|
+
cwd: string,
|
|
336
|
+
config: KimiCodeConfig,
|
|
337
|
+
): void {
|
|
338
|
+
if (scope === "project") {
|
|
339
|
+
saveProjectKimiCodeConfig(cwd, config);
|
|
340
|
+
} else {
|
|
341
|
+
saveHomeKimiCodeConfig(os.homedir(), config);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function buildKimiMainTitle(_config: KimiCodeConfig, _cwd: string): string {
|
|
346
|
+
return "Kimi settings";
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function buildConfigScopeTitle(
|
|
350
|
+
scope: KimiConfigScope,
|
|
351
|
+
config: KimiCodeConfig,
|
|
352
|
+
cwd: string,
|
|
353
|
+
): string {
|
|
354
|
+
const path =
|
|
355
|
+
scope === "project"
|
|
356
|
+
? relative(cwd, getProjectKimiCodeConfigPath(cwd))
|
|
357
|
+
: homeRelative(getGlobalKimiCodeConfigPath(os.homedir()));
|
|
358
|
+
return [`Edit ${scope} config`, `File: ${path}`, "", moonshotStatus(config)].join("\n");
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function homeRelative(filePath: string): string {
|
|
362
|
+
const home = os.homedir();
|
|
363
|
+
return filePath.startsWith(`${home}/`) ? `~/${filePath.slice(home.length + 1)}` : filePath;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function toolMenuItem(config: KimiCodeConfig, toolName: MoonshotToolName): string {
|
|
367
|
+
return `${toolName} -> ${formatToolStatus(config, toolName)}`;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function formatToolStatus(config: KimiCodeConfig, toolName: MoonshotToolName): string {
|
|
371
|
+
const tool = config.tools[toolName];
|
|
372
|
+
const enabled = tool.enabled ? "enabled" : "disabled";
|
|
373
|
+
const collapsed = tool.default_collapsed ? "default collapsed" : "default expanded";
|
|
374
|
+
return `${enabled}, ${collapsed}`;
|
|
375
|
+
}
|
|
38
376
|
|
|
39
377
|
export default function (pi: ExtensionAPI) {
|
|
378
|
+
const config = loadKimiCodeConfig({ cwd: process.cwd(), home: os.homedir() });
|
|
379
|
+
|
|
40
380
|
pi.registerProvider(PROVIDER_ID, {
|
|
41
381
|
baseUrl: getBaseUrl(),
|
|
42
382
|
apiKey: "KIMI_API_KEY",
|
|
@@ -76,4 +416,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
76
416
|
},
|
|
77
417
|
},
|
|
78
418
|
});
|
|
419
|
+
|
|
420
|
+
registerConfiguredMoonshotTools(pi, config, { updateActiveTools: false });
|
|
421
|
+
|
|
422
|
+
pi.registerCommand("kimi-settings", {
|
|
423
|
+
description: "Show Kimi usage and configure optional Kimi tools",
|
|
424
|
+
handler: async (_args, ctx) => {
|
|
425
|
+
await runKimiCommand(pi, ctx);
|
|
426
|
+
},
|
|
427
|
+
});
|
|
79
428
|
}
|
package/package.json
CHANGED
package/src/config.ts
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
|
|
4
|
+
export interface KimiCodeConfig {
|
|
5
|
+
tools: {
|
|
6
|
+
moonshot_search: { enabled: boolean; default_collapsed: boolean };
|
|
7
|
+
moonshot_fetch: { enabled: boolean; default_collapsed: boolean };
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface LoadKimiCodeConfigOptions {
|
|
12
|
+
cwd: string;
|
|
13
|
+
home: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const DEFAULT_KIMI_CODE_CONFIG: KimiCodeConfig = {
|
|
17
|
+
tools: {
|
|
18
|
+
moonshot_search: { enabled: false, default_collapsed: true },
|
|
19
|
+
moonshot_fetch: { enabled: false, default_collapsed: true },
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export function getGlobalKimiCodeConfigPath(home: string): string {
|
|
24
|
+
return join(home, ".pi", "pi-provider-kimi-code.json");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function getProjectKimiCodeConfigPath(cwd: string): string {
|
|
28
|
+
return join(cwd, ".pi", "pi-provider-kimi-code.json");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
32
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function readConfigFile(path: string): Record<string, unknown> {
|
|
36
|
+
try {
|
|
37
|
+
if (!existsSync(path)) return {};
|
|
38
|
+
const parsed = JSON.parse(readFileSync(path, "utf8")) as unknown;
|
|
39
|
+
return isRecord(parsed) ? parsed : {};
|
|
40
|
+
} catch (error) {
|
|
41
|
+
console.error(`[kimi-coding] failed to read config file ${path}:`, error);
|
|
42
|
+
return {};
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function readConfigFileQuiet(path: string): Record<string, unknown> {
|
|
47
|
+
try {
|
|
48
|
+
if (!existsSync(path)) return {};
|
|
49
|
+
const parsed = JSON.parse(readFileSync(path, "utf8")) as unknown;
|
|
50
|
+
return isRecord(parsed) ? parsed : {};
|
|
51
|
+
} catch {
|
|
52
|
+
return {};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function mergeConfig(
|
|
57
|
+
base: Record<string, unknown>,
|
|
58
|
+
override: Record<string, unknown>,
|
|
59
|
+
): Record<string, unknown> {
|
|
60
|
+
const result: Record<string, unknown> = { ...base };
|
|
61
|
+
for (const [key, value] of Object.entries(override)) {
|
|
62
|
+
const current = result[key];
|
|
63
|
+
if (isRecord(current) && isRecord(value)) {
|
|
64
|
+
result[key] = mergeConfig(current, value);
|
|
65
|
+
} else {
|
|
66
|
+
result[key] = value;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function readEnabled(config: Record<string, unknown>, toolName: string): boolean {
|
|
73
|
+
const tools = config.tools;
|
|
74
|
+
if (!isRecord(tools)) return false;
|
|
75
|
+
const tool = tools[toolName];
|
|
76
|
+
if (!isRecord(tool)) return false;
|
|
77
|
+
return tool.enabled === true;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function readDefaultCollapsed(config: Record<string, unknown>, toolName: string): boolean {
|
|
81
|
+
const tools = config.tools;
|
|
82
|
+
if (!isRecord(tools)) return true;
|
|
83
|
+
const tool = tools[toolName];
|
|
84
|
+
if (!isRecord(tool)) return true;
|
|
85
|
+
return tool.default_collapsed !== false;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function loadKimiCodeConfig(options: LoadKimiCodeConfigOptions): KimiCodeConfig {
|
|
89
|
+
const globalConfig = readConfigFile(getGlobalKimiCodeConfigPath(options.home));
|
|
90
|
+
const projectConfig = readConfigFile(getProjectKimiCodeConfigPath(options.cwd));
|
|
91
|
+
const merged = mergeConfig(globalConfig, projectConfig);
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
tools: {
|
|
95
|
+
moonshot_search: {
|
|
96
|
+
enabled: readEnabled(merged, "moonshot_search"),
|
|
97
|
+
default_collapsed: readDefaultCollapsed(merged, "moonshot_search"),
|
|
98
|
+
},
|
|
99
|
+
moonshot_fetch: {
|
|
100
|
+
enabled: readEnabled(merged, "moonshot_fetch"),
|
|
101
|
+
default_collapsed: readDefaultCollapsed(merged, "moonshot_fetch"),
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function loadProjectKimiCodeConfig(cwd: string): KimiCodeConfig {
|
|
108
|
+
return loadKimiCodeConfigFile(getProjectKimiCodeConfigPath(cwd));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function loadHomeKimiCodeConfig(home: string): KimiCodeConfig {
|
|
112
|
+
return loadKimiCodeConfigFile(getGlobalKimiCodeConfigPath(home));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function loadKimiCodeConfigFile(path: string): KimiCodeConfig {
|
|
116
|
+
const projectConfig = readConfigFileQuiet(path);
|
|
117
|
+
return {
|
|
118
|
+
tools: {
|
|
119
|
+
moonshot_search: {
|
|
120
|
+
enabled: readEnabled(projectConfig, "moonshot_search"),
|
|
121
|
+
default_collapsed: readDefaultCollapsed(projectConfig, "moonshot_search"),
|
|
122
|
+
},
|
|
123
|
+
moonshot_fetch: {
|
|
124
|
+
enabled: readEnabled(projectConfig, "moonshot_fetch"),
|
|
125
|
+
default_collapsed: readDefaultCollapsed(projectConfig, "moonshot_fetch"),
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function saveProjectKimiCodeConfig(cwd: string, config: KimiCodeConfig): void {
|
|
132
|
+
saveKimiCodeConfigFile(getProjectKimiCodeConfigPath(cwd), config);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function saveHomeKimiCodeConfig(home: string, config: KimiCodeConfig): void {
|
|
136
|
+
saveKimiCodeConfigFile(getGlobalKimiCodeConfigPath(home), config);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function saveKimiCodeConfigFile(path: string, config: KimiCodeConfig): void {
|
|
140
|
+
const raw = readConfigFileQuiet(path);
|
|
141
|
+
const next = {
|
|
142
|
+
...raw,
|
|
143
|
+
tools: {
|
|
144
|
+
...(isRecord(raw.tools) ? raw.tools : {}),
|
|
145
|
+
moonshot_search: { ...config.tools.moonshot_search },
|
|
146
|
+
moonshot_fetch: { ...config.tools.moonshot_fetch },
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
150
|
+
writeFileSync(path, `${JSON.stringify(next, null, 2)}\n`, "utf8");
|
|
151
|
+
}
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
import { AuthStorage, defineTool, type AgentToolResult } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Type, type Static } from "@earendil-works/pi-ai";
|
|
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;
|
|
9
|
+
|
|
10
|
+
export const moonshotSearchSchema = Type.Object({
|
|
11
|
+
query: Type.String({ description: "The query text to search for." }),
|
|
12
|
+
limit: Type.Optional(
|
|
13
|
+
Type.Number({
|
|
14
|
+
description: "The number of results to return.",
|
|
15
|
+
default: 5,
|
|
16
|
+
minimum: 1,
|
|
17
|
+
maximum: 20,
|
|
18
|
+
}),
|
|
19
|
+
),
|
|
20
|
+
include_content: Type.Optional(
|
|
21
|
+
Type.Boolean({
|
|
22
|
+
description: "Whether to include fetched page content in the search results.",
|
|
23
|
+
default: false,
|
|
24
|
+
}),
|
|
25
|
+
),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
export const moonshotFetchSchema = Type.Object({
|
|
29
|
+
url: Type.String({ description: "The URL to fetch content from." }),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
export type MoonshotSearchParams = Static<typeof moonshotSearchSchema>;
|
|
33
|
+
export type MoonshotFetchParams = Static<typeof moonshotFetchSchema>;
|
|
34
|
+
|
|
35
|
+
export interface MoonshotSearchResult {
|
|
36
|
+
url: string;
|
|
37
|
+
title: string;
|
|
38
|
+
snippet: string;
|
|
39
|
+
content?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface MoonshotFetchResult {
|
|
43
|
+
url: string;
|
|
44
|
+
content: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
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
|
+
function clampLimit(value: number | undefined): number {
|
|
75
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return 5;
|
|
76
|
+
return Math.max(1, Math.min(20, Math.trunc(value)));
|
|
77
|
+
}
|
|
78
|
+
|
|
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
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function stringField(record: Record<string, unknown>, key: string): string {
|
|
118
|
+
const value = record[key];
|
|
119
|
+
return typeof value === "string" ? value : "";
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function mapSearchResults(value: unknown, includeContent: boolean): MoonshotSearchResult[] {
|
|
123
|
+
if (!Array.isArray(value)) return [];
|
|
124
|
+
return value
|
|
125
|
+
.filter((item): item is Record<string, unknown> => typeof item === "object" && item !== null)
|
|
126
|
+
.map((item) => {
|
|
127
|
+
const result: MoonshotSearchResult = {
|
|
128
|
+
url: stringField(item, "url"),
|
|
129
|
+
title: stringField(item, "title"),
|
|
130
|
+
snippet: stringField(item, "snippet"),
|
|
131
|
+
};
|
|
132
|
+
if (includeContent) {
|
|
133
|
+
result.content = stringField(item, "content");
|
|
134
|
+
}
|
|
135
|
+
return result;
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function searchResultsText(results: MoonshotSearchResult[]): string {
|
|
140
|
+
if (results.length === 0) return "[]";
|
|
141
|
+
return JSON.stringify(results, null, 2);
|
|
142
|
+
}
|
|
143
|
+
|
|
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
|
+
function renderSearchResult(
|
|
161
|
+
result: AgentToolResult<MoonshotSearchResult[]>,
|
|
162
|
+
expanded: boolean | undefined,
|
|
163
|
+
defaultCollapsed: boolean,
|
|
164
|
+
) {
|
|
165
|
+
if (!Array.isArray(result.details)) {
|
|
166
|
+
return textComponent(firstText(result));
|
|
167
|
+
}
|
|
168
|
+
if (!shouldCollapse(defaultCollapsed, expanded)) {
|
|
169
|
+
return textComponent(searchResultsText(result.details));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const count = result.details.length;
|
|
173
|
+
const first = result.details[0];
|
|
174
|
+
const suffix = first ? `; first: ${first.title || first.url}` : "";
|
|
175
|
+
return textComponent(`moonshot_search returned ${count} result(s)${suffix}`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function renderFetchResult(
|
|
179
|
+
result: AgentToolResult<MoonshotFetchResult>,
|
|
180
|
+
expanded: boolean | undefined,
|
|
181
|
+
defaultCollapsed: boolean,
|
|
182
|
+
) {
|
|
183
|
+
if (!result.details) {
|
|
184
|
+
return textComponent(firstText(result));
|
|
185
|
+
}
|
|
186
|
+
if (!shouldCollapse(defaultCollapsed, expanded)) {
|
|
187
|
+
return textComponent(result.details.content);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return textComponent(
|
|
191
|
+
`moonshot_fetch fetched ${result.details.url} (${result.details.content.length} chars)`,
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
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);
|
|
201
|
+
const defaultCollapsed = options.defaultCollapsed ?? true;
|
|
202
|
+
|
|
203
|
+
return defineTool({
|
|
204
|
+
name: "moonshot_search",
|
|
205
|
+
label: "Moonshot Search",
|
|
206
|
+
description: "Search the web through Kimi Coding's server-side Moonshot search service.",
|
|
207
|
+
promptSnippet: "Search the web with Kimi Coding's Moonshot search service",
|
|
208
|
+
parameters: moonshotSearchSchema,
|
|
209
|
+
|
|
210
|
+
async execute(toolCallId, params, signal) {
|
|
211
|
+
const accessToken = deps.getAccessToken();
|
|
212
|
+
if (!accessToken) {
|
|
213
|
+
return errorResult<MoonshotSearchResult[]>(
|
|
214
|
+
"Missing Kimi Code OAuth credentials. Run /login kimi-coding first.",
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const limit = clampLimit(params.limit);
|
|
219
|
+
const includeContent = params.include_content === true;
|
|
220
|
+
const timeout = buildTimeoutSignal(signal);
|
|
221
|
+
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,
|
|
230
|
+
}),
|
|
231
|
+
signal: timeout.signal,
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
if (!response.ok) {
|
|
235
|
+
const body = await readErrorBody(response);
|
|
236
|
+
return errorResult<MoonshotSearchResult[]>(
|
|
237
|
+
`Moonshot search failed: ${response.status}${body ? ` ${body}` : ""}`,
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const data = (await response.json()) as MoonshotSearchResponse;
|
|
242
|
+
const results = mapSearchResults(data.search_results, includeContent);
|
|
243
|
+
return {
|
|
244
|
+
content: [{ type: "text", text: searchResultsText(results) }],
|
|
245
|
+
details: results,
|
|
246
|
+
};
|
|
247
|
+
} catch (error) {
|
|
248
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
249
|
+
return errorResult<MoonshotSearchResult[]>(`Moonshot search failed: ${message}`);
|
|
250
|
+
} finally {
|
|
251
|
+
timeout.cleanup();
|
|
252
|
+
}
|
|
253
|
+
},
|
|
254
|
+
|
|
255
|
+
renderResult(result, renderOptions) {
|
|
256
|
+
return renderSearchResult(result, renderOptions.expanded, defaultCollapsed);
|
|
257
|
+
},
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export function buildMoonshotFetchTool(options: BuildMoonshotToolOptions = {}) {
|
|
262
|
+
const deps = buildDeps(options);
|
|
263
|
+
const defaultCollapsed = options.defaultCollapsed ?? true;
|
|
264
|
+
|
|
265
|
+
return defineTool({
|
|
266
|
+
name: "moonshot_fetch",
|
|
267
|
+
label: "Moonshot Fetch",
|
|
268
|
+
description: "Fetch a web page through Kimi Coding's server-side Moonshot fetch service.",
|
|
269
|
+
promptSnippet: "Fetch a web page with Kimi Coding's Moonshot fetch service",
|
|
270
|
+
parameters: moonshotFetchSchema,
|
|
271
|
+
|
|
272
|
+
async execute(toolCallId, params, signal) {
|
|
273
|
+
const accessToken = deps.getAccessToken();
|
|
274
|
+
if (!accessToken) {
|
|
275
|
+
return errorResult<MoonshotFetchResult>(
|
|
276
|
+
"Missing Kimi Code OAuth credentials. Run /login kimi-coding first.",
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const timeout = buildTimeoutSignal(signal);
|
|
281
|
+
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
|
+
});
|
|
291
|
+
|
|
292
|
+
if (!response.ok) {
|
|
293
|
+
const body = await readErrorBody(response);
|
|
294
|
+
return errorResult<MoonshotFetchResult>(
|
|
295
|
+
`Moonshot fetch failed: ${response.status}${body ? ` ${body}` : ""}`,
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const content = await response.text();
|
|
300
|
+
return {
|
|
301
|
+
content: [{ type: "text", text: content }],
|
|
302
|
+
details: { url: params.url, content },
|
|
303
|
+
};
|
|
304
|
+
} catch (error) {
|
|
305
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
306
|
+
return errorResult<MoonshotFetchResult>(`Moonshot fetch failed: ${message}`);
|
|
307
|
+
} finally {
|
|
308
|
+
timeout.cleanup();
|
|
309
|
+
}
|
|
310
|
+
},
|
|
311
|
+
|
|
312
|
+
renderResult(result, renderOptions) {
|
|
313
|
+
return renderFetchResult(result, renderOptions.expanded, defaultCollapsed);
|
|
314
|
+
},
|
|
315
|
+
});
|
|
316
|
+
}
|