ladder-mcp 1.0.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/CHANGELOG.md +63 -0
- package/LICENSE +27 -0
- package/README.md +85 -0
- package/dist/desktop-work.js +102 -0
- package/dist/environment.js +193 -0
- package/dist/index.js +289 -0
- package/dist/kimi-api.js +72 -0
- package/dist/kimi-mcp-config.js +85 -0
- package/dist/kimi-runner.js +142 -0
- package/dist/session-store.js +122 -0
- package/dist/task-store.js +164 -0
- package/dist/transports/acp.js +415 -0
- package/dist/transports/cli-admin.js +239 -0
- package/dist/types.js +1 -0
- package/package.json +37 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to Ladder_mcp are documented here. Format loosely follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/); this project uses
|
|
5
|
+
[Semantic Versioning](https://semver.org/).
|
|
6
|
+
|
|
7
|
+
## [1.0.0] - 2026-06-27
|
|
8
|
+
|
|
9
|
+
First release. Windows-first MCP bridge for Kimi Code CLI v24, rebuilt in a
|
|
10
|
+
fresh `./src` application (package `ladder-mcp`). Logic is ported from the
|
|
11
|
+
read-only `kimi-code-mcp/` reference; the legacy `CacheManager`/warmup layer is
|
|
12
|
+
not carried over.
|
|
13
|
+
|
|
14
|
+
### Added — v1 core (Epics 1-3)
|
|
15
|
+
|
|
16
|
+
- Robust environment resolver (`environment.ts`): discovers `kimi.exe` via PATH
|
|
17
|
+
and `~/.kimi-code/bin/`, resolves the `~/.kimi-code/` catalog, credentials and
|
|
18
|
+
version with no hardcoded POSIX paths; no silent fallback to legacy `~/.kimi/`.
|
|
19
|
+
- CLI adapter (`kimi-runner.ts`): correct Kimi CLI v24 arguments
|
|
20
|
+
(`-p`, `--output-format stream-json`, `-S`, `-C`, `--auto`), `stream-json`
|
|
21
|
+
parsing, native session resume, Windows process-tree termination on timeout.
|
|
22
|
+
- API adapter (`kimi-api.ts`): contextless `kimi_query` / `kimi_verify` reading
|
|
23
|
+
key/endpoint from `KIMICODE_API_KEY` or `~/.kimi-code/config.toml`.
|
|
24
|
+
- Six v1 MCP tools: `kimi_analyze`, `kimi_query`, `kimi_verify`, `kimi_resume`,
|
|
25
|
+
`kimi_list_sessions`, `kimi_status`.
|
|
26
|
+
|
|
27
|
+
### Added — vNext expansion (Epic 4)
|
|
28
|
+
|
|
29
|
+
- **CLI admin & capabilities** (`transports/cli-admin.ts`): `kimi_capabilities`,
|
|
30
|
+
`kimi_doctor`, `kimi_provider_list`, `kimi_export_session`,
|
|
31
|
+
`kimi_visualize_session`. Export requires an explicit `output_path`, is
|
|
32
|
+
confined to the working directory, and excludes the global diagnostic log by
|
|
33
|
+
default.
|
|
34
|
+
- **ACP MVP over stdio** (`transports/acp.ts`): JSON-RPC client for `kimi acp`
|
|
35
|
+
with newline-delimited and `Content-Length` framing; `kimi_chat`,
|
|
36
|
+
`kimi_acp_sessions`, `kimi_cancel`.
|
|
37
|
+
- **Background task lifecycle** (`task-store.ts`): in-process task store with
|
|
38
|
+
`kimi_chat background=true`, `kimi_task_status`, `kimi_task_output`,
|
|
39
|
+
`kimi_task_cancel`.
|
|
40
|
+
- **Kimi-hosted MCP config + read-only desktop probes**
|
|
41
|
+
(`kimi-mcp-config.ts`, `desktop-work.ts`): `kimi_generate_mcp_config`,
|
|
42
|
+
`kimi_desktop_status`, `kimi_budget_probe`. Desktop access is
|
|
43
|
+
experimental/read-only — no token-store reads, no web-auth replay, no desktop
|
|
44
|
+
Work task submission.
|
|
45
|
+
|
|
46
|
+
### Security & robustness (adversarial review)
|
|
47
|
+
|
|
48
|
+
Hardened against path traversal/TOCTOU on export, writes under the read-only
|
|
49
|
+
reference tree, unbounded ACP frame/task memory, process crash on malformed ACP
|
|
50
|
+
JSON, non-idempotent task cancellation, and UTF-8 byte-boundary truncation.
|
|
51
|
+
|
|
52
|
+
The remaining lower-risk review findings (W1-W12) were also resolved before
|
|
53
|
+
release: raw ACP response-id matching (no `NaN` coercion), bounded ACP update
|
|
54
|
+
buffer, clamped ACP request timeouts, smaller buffers for parallel capability
|
|
55
|
+
probes, a timeout around task cancel hooks, a size cap on the desktop status
|
|
56
|
+
probe body, validated `kimi_chat` timeout input, and `kimi_cancel` rejecting
|
|
57
|
+
ambiguous task_id+session_id calls. Regression tests added (49 total).
|
|
58
|
+
|
|
59
|
+
### Known limitations
|
|
60
|
+
|
|
61
|
+
- Windows 11 only (NFR-1); POSIX branches are not a target.
|
|
62
|
+
|
|
63
|
+
[1.0.0]: https://semver.org/
|
package/LICENSE
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sherstnev
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
This project ports logic from the kimi-code-mcp reference
|
|
26
|
+
(MIT License, Copyright (c) 2026 howardpen9), retained as a read-only
|
|
27
|
+
reference under ./kimi-code-mcp/.
|
package/README.md
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# Ladder_mcp
|
|
2
|
+
|
|
3
|
+
Windows-first [MCP](https://modelcontextprotocol.io/) bridge for the
|
|
4
|
+
[Kimi Code](https://kimi.com) CLI (v24). It exposes Kimi Code as MCP tools so a
|
|
5
|
+
client like Claude Code can run codebase analysis, native sessions, API
|
|
6
|
+
queries, ACP chat, background tasks, and CLI admin/diagnostics — all on Windows
|
|
7
|
+
without hardcoded POSIX assumptions.
|
|
8
|
+
|
|
9
|
+
> Status: **v1.0.0**. Supported platform is **Windows 11 only**.
|
|
10
|
+
|
|
11
|
+
## Requirements
|
|
12
|
+
|
|
13
|
+
- Windows 11
|
|
14
|
+
- Node.js ≥ 18
|
|
15
|
+
- Kimi Code CLI installed (`kimi.exe` on PATH or at `~/.kimi-code/bin/kimi.exe`),
|
|
16
|
+
authenticated (`~/.kimi-code/`)
|
|
17
|
+
|
|
18
|
+
## Install & build
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install
|
|
22
|
+
npm run build # compiles src/ -> dist/ (tests excluded)
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Quick checks:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm test # vitest (44 tests)
|
|
29
|
+
npm run typecheck # tsc --noEmit (incl. tests)
|
|
30
|
+
npm run dev # run the server from source via tsx
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Run as an MCP server
|
|
34
|
+
|
|
35
|
+
The server speaks MCP over stdio. Point your MCP client at the built entry:
|
|
36
|
+
|
|
37
|
+
```jsonc
|
|
38
|
+
// e.g. Claude Code MCP config
|
|
39
|
+
{
|
|
40
|
+
"mcpServers": {
|
|
41
|
+
"kimi-code": {
|
|
42
|
+
"command": "node",
|
|
43
|
+
"args": ["C:\\path\\to\\Ladder_mcp\\dist\\index.js"]
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
To let Kimi Code itself host this server, use the `kimi_generate_mcp_config`
|
|
50
|
+
tool to produce/merge a `.kimi-code/mcp.json` entry.
|
|
51
|
+
|
|
52
|
+
## Tools
|
|
53
|
+
|
|
54
|
+
**Core (v1)** — `kimi_analyze`, `kimi_query`, `kimi_verify`, `kimi_resume`,
|
|
55
|
+
`kimi_list_sessions`, `kimi_status`
|
|
56
|
+
|
|
57
|
+
**CLI admin & capabilities** — `kimi_capabilities`, `kimi_doctor`,
|
|
58
|
+
`kimi_provider_list`, `kimi_export_session`, `kimi_visualize_session`
|
|
59
|
+
|
|
60
|
+
**ACP (chat over stdio)** — `kimi_chat`, `kimi_acp_sessions`, `kimi_cancel`
|
|
61
|
+
|
|
62
|
+
**Background tasks** — `kimi_task_status`, `kimi_task_output`, `kimi_task_cancel`
|
|
63
|
+
(set `background=true` on `kimi_chat`)
|
|
64
|
+
|
|
65
|
+
**Kimi-hosted config & desktop (read-only)** — `kimi_generate_mcp_config`,
|
|
66
|
+
`kimi_desktop_status`, `kimi_budget_probe`
|
|
67
|
+
|
|
68
|
+
## Safety boundaries
|
|
69
|
+
|
|
70
|
+
- `kimi_export_session` requires an explicit `output_path`, stays within the
|
|
71
|
+
working directory, and excludes the global diagnostic log by default.
|
|
72
|
+
- Desktop Work tools are **experimental and read-only**: they do not read the
|
|
73
|
+
desktop token store, replay web auth, or submit desktop Work tasks.
|
|
74
|
+
- The vendored `kimi-code-mcp/` is a **read-only reference** and is never edited
|
|
75
|
+
or written to by the tools.
|
|
76
|
+
|
|
77
|
+
## Project layout
|
|
78
|
+
|
|
79
|
+
- `src/` — the Ladder_mcp application (package `ladder-mcp`)
|
|
80
|
+
- `kimi-code-mcp/` — upstream reference (read-only, MIT)
|
|
81
|
+
- `_bmad-output/` — planning & implementation artifacts (BMAD workflow)
|
|
82
|
+
|
|
83
|
+
## License
|
|
84
|
+
|
|
85
|
+
[MIT](./LICENSE). Ports logic from the MIT-licensed `kimi-code-mcp` reference.
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
const execFileAsync = promisify(execFile);
|
|
4
|
+
export const MAX_PROBE_BODY_BYTES = 256 * 1024;
|
|
5
|
+
// Read a fetch response body but stop once MAX_PROBE_BODY_BYTES is reached, so a
|
|
6
|
+
// misbehaving or hostile local endpoint cannot stream an unbounded body into memory.
|
|
7
|
+
// The surrounding AbortController still bounds total time.
|
|
8
|
+
export async function readBodyCapped(response) {
|
|
9
|
+
const reader = response.body?.getReader();
|
|
10
|
+
if (!reader)
|
|
11
|
+
return { text: await response.text(), truncated: false };
|
|
12
|
+
const decoder = new TextDecoder();
|
|
13
|
+
let text = '';
|
|
14
|
+
let total = 0;
|
|
15
|
+
let truncated = false;
|
|
16
|
+
for (;;) {
|
|
17
|
+
const { done, value } = await reader.read();
|
|
18
|
+
if (done)
|
|
19
|
+
break;
|
|
20
|
+
total += value.byteLength;
|
|
21
|
+
if (total > MAX_PROBE_BODY_BYTES) {
|
|
22
|
+
text += decoder.decode(value.slice(0, Math.max(0, MAX_PROBE_BODY_BYTES - (total - value.byteLength))));
|
|
23
|
+
truncated = true;
|
|
24
|
+
await reader.cancel();
|
|
25
|
+
break;
|
|
26
|
+
}
|
|
27
|
+
text += decoder.decode(value, { stream: true });
|
|
28
|
+
}
|
|
29
|
+
if (!truncated)
|
|
30
|
+
text += decoder.decode();
|
|
31
|
+
return { text, truncated };
|
|
32
|
+
}
|
|
33
|
+
export async function getDesktopStatus() {
|
|
34
|
+
const safety = [
|
|
35
|
+
'Experimental/read-only probe only.',
|
|
36
|
+
'Does not read desktop token-store files.',
|
|
37
|
+
'Does not replay web auth.',
|
|
38
|
+
'Does not submit desktop Work tasks.',
|
|
39
|
+
];
|
|
40
|
+
const status = { experimental: true, readOnly: true, safety };
|
|
41
|
+
try {
|
|
42
|
+
// Resolved from PATH. This is safe by construction: execFile runs the binary
|
|
43
|
+
// directly (no shell, so no metacharacter injection), the only argument is the
|
|
44
|
+
// fixed read-only `status` verb, and a missing binary simply rejects below.
|
|
45
|
+
const { stdout, stderr } = await execFileAsync('kimi-webbridge', ['status'], {
|
|
46
|
+
timeout: 5000,
|
|
47
|
+
windowsHide: true,
|
|
48
|
+
});
|
|
49
|
+
status.bridgeCommand = { ok: true, stdout: String(stdout), stderr: String(stderr) };
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
status.bridgeCommand = { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
const controller = new AbortController();
|
|
56
|
+
const timer = setTimeout(() => controller.abort(), 3000);
|
|
57
|
+
try {
|
|
58
|
+
const response = await fetch('http://127.0.0.1:10086/status', { signal: controller.signal });
|
|
59
|
+
const { text, truncated } = await readBodyCapped(response);
|
|
60
|
+
let body = text;
|
|
61
|
+
if (!truncated) {
|
|
62
|
+
try {
|
|
63
|
+
body = JSON.parse(text);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
// Keep raw text.
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
status.bridgeHttp = { ok: response.ok, status: response.status, body, ...(truncated ? { truncated: true } : {}) };
|
|
70
|
+
}
|
|
71
|
+
finally {
|
|
72
|
+
clearTimeout(timer);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
status.bridgeHttp = { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
77
|
+
}
|
|
78
|
+
return status;
|
|
79
|
+
}
|
|
80
|
+
export function buildBudgetProbeGuide(includeCliProbe) {
|
|
81
|
+
const lines = [
|
|
82
|
+
'## Kimi Budget Probe (experimental evidence workflow)',
|
|
83
|
+
'',
|
|
84
|
+
'Safety boundaries:',
|
|
85
|
+
'- This probe does not read desktop token-store files.',
|
|
86
|
+
'- This probe does not replay web auth.',
|
|
87
|
+
'- This probe does not submit desktop Work tasks.',
|
|
88
|
+
'- Treat budget separation as unproven until visible counters confirm it.',
|
|
89
|
+
'',
|
|
90
|
+
'Manual evidence steps:',
|
|
91
|
+
'1. Record visible Kimi Code CLI usage/billing counters.',
|
|
92
|
+
'2. Record visible Desktop/OK Computer/Kimi Work counters from the app UI.',
|
|
93
|
+
'3. Run one tiny CLI prompt.',
|
|
94
|
+
'4. Run one tiny Desktop Work task manually in the visible app.',
|
|
95
|
+
'5. Compare which counters moved after refresh.',
|
|
96
|
+
'6. Repeat once to rule out delayed accounting.',
|
|
97
|
+
];
|
|
98
|
+
if (includeCliProbe) {
|
|
99
|
+
lines.push('', 'CLI probe requested: run a tiny `kimi_chat`/`kimi_analyze` prompt separately and compare visible counters.');
|
|
100
|
+
}
|
|
101
|
+
return lines.join('\n');
|
|
102
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import { promisify } from 'node:util';
|
|
5
|
+
const execFileAsync = promisify(execFile);
|
|
6
|
+
const DEFAULT_BASE_URL = 'https://api.kimi.com/coding/v1';
|
|
7
|
+
function getEnv(options) {
|
|
8
|
+
return options?.env ?? process.env;
|
|
9
|
+
}
|
|
10
|
+
function exists(candidate, options) {
|
|
11
|
+
return options?.existsSync ? options.existsSync(candidate) : fs.existsSync(candidate);
|
|
12
|
+
}
|
|
13
|
+
function readText(candidate, options) {
|
|
14
|
+
return (options?.readFileSync ?? fs.readFileSync)(candidate, 'utf-8');
|
|
15
|
+
}
|
|
16
|
+
export function getWindowsHome(env = process.env) {
|
|
17
|
+
if (env.USERPROFILE)
|
|
18
|
+
return env.USERPROFILE;
|
|
19
|
+
if (env.HOMEDRIVE && env.HOMEPATH)
|
|
20
|
+
return `${env.HOMEDRIVE}${env.HOMEPATH}`;
|
|
21
|
+
throw new Error('USERPROFILE is not set; Ladder_mcp v1 supports Windows only.');
|
|
22
|
+
}
|
|
23
|
+
function splitWindowsPath(pathValue) {
|
|
24
|
+
return (pathValue ?? '').split(';').map((part) => part.trim()).filter(Boolean);
|
|
25
|
+
}
|
|
26
|
+
export function findKimiOnPath(env = process.env, existsSync = fs.existsSync) {
|
|
27
|
+
for (const entry of splitWindowsPath(env.PATH ?? env.Path)) {
|
|
28
|
+
const exe = path.join(entry, 'kimi.exe');
|
|
29
|
+
if (existsSync(exe))
|
|
30
|
+
return exe;
|
|
31
|
+
}
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
export function resolveKimiPaths(options) {
|
|
35
|
+
const env = getEnv(options);
|
|
36
|
+
const homeDir = getWindowsHome(env);
|
|
37
|
+
const kimiDir = path.join(homeDir, '.kimi-code');
|
|
38
|
+
const defaultBinary = path.join(kimiDir, 'bin', 'kimi.exe');
|
|
39
|
+
const pathBinary = findKimiOnPath(env, (candidate) => exists(candidate, options));
|
|
40
|
+
const binaryPath = pathBinary ?? (exists(defaultBinary, options) ? defaultBinary : undefined);
|
|
41
|
+
return {
|
|
42
|
+
homeDir,
|
|
43
|
+
kimiDir,
|
|
44
|
+
configPath: path.join(kimiDir, 'config.toml'),
|
|
45
|
+
credentialsPath: path.join(kimiDir, 'credentials', 'kimi-code.json'),
|
|
46
|
+
sessionsDir: path.join(kimiDir, 'sessions'),
|
|
47
|
+
sessionIndexPath: path.join(kimiDir, 'session_index.jsonl'),
|
|
48
|
+
legacyDir: path.join(homeDir, '.kimi'),
|
|
49
|
+
pathBinary,
|
|
50
|
+
defaultBinary,
|
|
51
|
+
binaryPath,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
export function interpolateEnv(value, env = process.env) {
|
|
55
|
+
return value.replace(/\$\{(\w+)\}/g, (_, name) => env[name] ?? '');
|
|
56
|
+
}
|
|
57
|
+
function readTomlString(raw, key) {
|
|
58
|
+
const pattern = new RegExp(`^\\s*${key}\\s*=\\s*["']([^"']*)["']`, 'm');
|
|
59
|
+
return raw.match(pattern)?.[1];
|
|
60
|
+
}
|
|
61
|
+
export function loadApiAuth(options) {
|
|
62
|
+
const env = getEnv(options);
|
|
63
|
+
const paths = resolveKimiPaths(options);
|
|
64
|
+
let apiKey = env.KIMICODE_API_KEY ?? '';
|
|
65
|
+
let baseUrl = env.KIMI_BASE_URL ?? '';
|
|
66
|
+
if ((!apiKey || !baseUrl) && exists(paths.configPath, options)) {
|
|
67
|
+
try {
|
|
68
|
+
const raw = readText(paths.configPath, options);
|
|
69
|
+
if (!apiKey)
|
|
70
|
+
apiKey = readTomlString(raw, 'api_key') ?? '';
|
|
71
|
+
if (!baseUrl)
|
|
72
|
+
baseUrl = readTomlString(raw, 'base_url') ?? '';
|
|
73
|
+
if (!apiKey || !baseUrl) {
|
|
74
|
+
const providers = parseProviderBlocks(raw);
|
|
75
|
+
const coding = providers.find((provider) => provider.baseUrl?.includes('/coding'));
|
|
76
|
+
const chosen = coding ?? providers.find((provider) => provider.apiKey) ?? providers[0];
|
|
77
|
+
if (chosen) {
|
|
78
|
+
if (!apiKey && chosen.apiKey)
|
|
79
|
+
apiKey = chosen.apiKey;
|
|
80
|
+
if (!baseUrl && chosen.baseUrl)
|
|
81
|
+
baseUrl = chosen.baseUrl;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
// Environment variables are sufficient; malformed config simply means no file-based auth.
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
apiKey = interpolateEnv(apiKey, env).trim();
|
|
90
|
+
baseUrl = interpolateEnv(baseUrl || DEFAULT_BASE_URL, env).trim().replace(/\/$/, '');
|
|
91
|
+
return apiKey ? { apiKey, baseUrl } : null;
|
|
92
|
+
}
|
|
93
|
+
function parseProviderBlocks(raw) {
|
|
94
|
+
const providers = [];
|
|
95
|
+
let current = null;
|
|
96
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
97
|
+
const trimmed = line.trim();
|
|
98
|
+
if (!trimmed || trimmed.startsWith('#'))
|
|
99
|
+
continue;
|
|
100
|
+
const section = trimmed.match(/^\[(.+?)]$/);
|
|
101
|
+
if (section) {
|
|
102
|
+
current = section[1].startsWith('providers.') ? {} : null;
|
|
103
|
+
if (current)
|
|
104
|
+
providers.push(current);
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (!current)
|
|
108
|
+
continue;
|
|
109
|
+
const kv = trimmed.match(/^(\w+)\s*=\s*["']([^"']*)["']/);
|
|
110
|
+
if (!kv)
|
|
111
|
+
continue;
|
|
112
|
+
if (kv[1] === 'api_key')
|
|
113
|
+
current.apiKey = kv[2];
|
|
114
|
+
if (kv[1] === 'base_url')
|
|
115
|
+
current.baseUrl = kv[2];
|
|
116
|
+
}
|
|
117
|
+
return providers;
|
|
118
|
+
}
|
|
119
|
+
export function isAuthenticated(options) {
|
|
120
|
+
const paths = resolveKimiPaths(options);
|
|
121
|
+
if (!exists(paths.credentialsPath, options))
|
|
122
|
+
return false;
|
|
123
|
+
try {
|
|
124
|
+
const raw = readText(paths.credentialsPath, options);
|
|
125
|
+
const data = JSON.parse(raw);
|
|
126
|
+
return Boolean(data.access_token || data.refresh_token || data.id_token || data.token);
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
export function isKimiInstalled(options) {
|
|
133
|
+
return Boolean(resolveKimiPaths(options).binaryPath);
|
|
134
|
+
}
|
|
135
|
+
export async function getKimiVersion(binaryPath, env = process.env) {
|
|
136
|
+
try {
|
|
137
|
+
const { stdout } = await execFileAsync(binaryPath, ['--version'], {
|
|
138
|
+
env: buildKimiEnv(env),
|
|
139
|
+
timeout: 5000,
|
|
140
|
+
windowsHide: true,
|
|
141
|
+
});
|
|
142
|
+
return String(stdout).trim() || undefined;
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
export function buildKimiEnv(env = process.env) {
|
|
149
|
+
const paths = resolveKimiPaths({ env });
|
|
150
|
+
const currentPath = env.PATH ?? env.Path ?? '';
|
|
151
|
+
const binDir = path.dirname(paths.defaultBinary);
|
|
152
|
+
const pathName = env.PATH !== undefined ? 'PATH' : 'Path';
|
|
153
|
+
const nextEnv = { ...env };
|
|
154
|
+
const entries = splitWindowsPath(currentPath);
|
|
155
|
+
if (!entries.some((entry) => path.normalize(entry).toLowerCase() === path.normalize(binDir).toLowerCase())) {
|
|
156
|
+
nextEnv[pathName] = `${binDir}${currentPath ? `;${currentPath}` : ''}`;
|
|
157
|
+
}
|
|
158
|
+
return nextEnv;
|
|
159
|
+
}
|
|
160
|
+
export async function getKimiStatus(options) {
|
|
161
|
+
const paths = resolveKimiPaths(options);
|
|
162
|
+
const catalogFound = exists(paths.kimiDir, options);
|
|
163
|
+
const credentialsFound = exists(paths.credentialsPath, options);
|
|
164
|
+
const configFound = exists(paths.configPath, options);
|
|
165
|
+
const apiConfigured = loadApiAuth(options) !== null;
|
|
166
|
+
const installed = Boolean(paths.binaryPath);
|
|
167
|
+
const authenticated = isAuthenticated(options);
|
|
168
|
+
let error;
|
|
169
|
+
if (!catalogFound) {
|
|
170
|
+
error = exists(paths.legacyDir, options)
|
|
171
|
+
? 'Found legacy ~/.kimi but not ~/.kimi-code. Update Kimi CLI to the current kimi-code version.'
|
|
172
|
+
: 'Kimi catalog ~/.kimi-code was not found. Install or update Kimi Code CLI.';
|
|
173
|
+
}
|
|
174
|
+
else if (!installed) {
|
|
175
|
+
error = 'Kimi CLI binary was not found on PATH or at ~/.kimi-code/bin/kimi.exe.';
|
|
176
|
+
}
|
|
177
|
+
else if (!authenticated) {
|
|
178
|
+
error = 'Kimi CLI is not authenticated. Run: kimi login';
|
|
179
|
+
}
|
|
180
|
+
const version = paths.binaryPath ? await getKimiVersion(paths.binaryPath, getEnv(options)) : undefined;
|
|
181
|
+
return {
|
|
182
|
+
installed,
|
|
183
|
+
binPath: paths.binaryPath ?? paths.defaultBinary,
|
|
184
|
+
version,
|
|
185
|
+
authenticated,
|
|
186
|
+
catalogFound,
|
|
187
|
+
catalogPath: paths.kimiDir,
|
|
188
|
+
configFound,
|
|
189
|
+
credentialsFound,
|
|
190
|
+
apiConfigured,
|
|
191
|
+
error,
|
|
192
|
+
};
|
|
193
|
+
}
|