ladder-mcp 1.0.0 → 1.0.2
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 +52 -0
- package/README.md +35 -16
- package/dist/environment.js +4 -1
- package/dist/index.js +13 -7
- package/dist/input-validation.js +29 -0
- package/dist/kimi-mcp-config.js +16 -1
- package/dist/kimi-runner.js +12 -2
- package/dist/transports/acp.js +27 -4
- package/dist/transports/cli-admin.js +31 -3
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,58 @@ All notable changes to Ladder_mcp are documented here. Format loosely follows
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/); this project uses
|
|
5
5
|
[Semantic Versioning](https://semver.org/).
|
|
6
6
|
|
|
7
|
+
## [1.0.2] - 2026-06-28
|
|
8
|
+
|
|
9
|
+
Security and robustness patch release addressing findings from an external
|
|
10
|
+
adversarial review of `src/`.
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **`isAuthenticated` false-positive on corrupt credentials** (`environment.ts`):
|
|
15
|
+
a credentials file that exists but is unreadable or invalid JSON was previously
|
|
16
|
+
reported as authenticated. It now fails closed and returns `false`, satisfying
|
|
17
|
+
NFR-5 honest diagnostics.
|
|
18
|
+
- **Unbounded `stdout`/`stderr` capture in `runKimi`** (`kimi-runner.ts`): CLI
|
|
19
|
+
output is now accumulated through `appendCapped` with a hard ceiling
|
|
20
|
+
(`MAX_CAPTURE_CHARS`), preventing memory exhaustion on runaway output.
|
|
21
|
+
- **Unvalidated `max_output_tokens` and `work_dir`** (`input-validation.ts`):
|
|
22
|
+
non-finite, zero, or negative token counts are clamped to the default budget;
|
|
23
|
+
`work_dir` must be an absolute path to an existing directory before Kimi is
|
|
24
|
+
spawned. Used by `kimi_analyze` and `kimi_resume`.
|
|
25
|
+
|
|
26
|
+
### Security
|
|
27
|
+
|
|
28
|
+
- **Arbitrary command persisted to `mcp.json`** (`kimi-mcp-config.ts`):
|
|
29
|
+
`kimi_generate_mcp_config` now validates `command` against an allow-list of
|
|
30
|
+
`node`, `npx`, or an absolute path to an existing file. Other values (e.g.
|
|
31
|
+
`powershell`) are rejected and nothing is written.
|
|
32
|
+
|
|
33
|
+
## [1.0.1] - 2026-06-28
|
|
34
|
+
|
|
35
|
+
Bug-fix release. Issues found by exercising all 20 tools live against a real
|
|
36
|
+
Kimi CLI (the mock-only unit tests had missed them).
|
|
37
|
+
|
|
38
|
+
### Fixed
|
|
39
|
+
|
|
40
|
+
- **ACP responses lost spaces** (`kimi_chat`): streamed token chunks were each
|
|
41
|
+
trimmed and newline-joined, so `"Two plus two equals four."` came back as
|
|
42
|
+
`"Twoplustwoequalsfour."`. Text fragments are now concatenated as-is and only
|
|
43
|
+
the final string is trimmed. (CLI tools `kimi_analyze`/`kimi_resume` were never
|
|
44
|
+
affected.)
|
|
45
|
+
- **`kimi_export_session` silently no-op'd** while reporting `ok: true`: when no
|
|
46
|
+
session id was given, `kimi export` defaults to the most recent session and asks
|
|
47
|
+
`Export previous session …? [Y/n]` — a prompt that `-y` does not suppress in Kimi
|
|
48
|
+
CLI 0.20.1, so with stdin closed it exited 0 without writing. The tool now
|
|
49
|
+
resolves the most recent session id itself and passes it explicitly (skipping the
|
|
50
|
+
prompt), always passes `-y`, and verifies the output file exists before reporting
|
|
51
|
+
success.
|
|
52
|
+
|
|
53
|
+
### Changed
|
|
54
|
+
|
|
55
|
+
- `kimi_acp_sessions` now accepts `limit` (default 20) and `work_dir` filters,
|
|
56
|
+
for parity with `kimi_list_sessions`; previously it dumped every ACP session
|
|
57
|
+
across all projects in one response.
|
|
58
|
+
|
|
7
59
|
## [1.0.0] - 2026-06-27
|
|
8
60
|
|
|
9
61
|
First release. Windows-first MCP bridge for Kimi Code CLI v24, rebuilt in a
|
package/README.md
CHANGED
|
@@ -6,7 +6,8 @@ client like Claude Code can run codebase analysis, native sessions, API
|
|
|
6
6
|
queries, ACP chat, background tasks, and CLI admin/diagnostics — all on Windows
|
|
7
7
|
without hardcoded POSIX assumptions.
|
|
8
8
|
|
|
9
|
-
> Status: **v1.0.
|
|
9
|
+
> Status: **v1.0.1** ([npm](https://www.npmjs.com/package/ladder-mcp)).
|
|
10
|
+
> Supported platform is **Windows 11 only**.
|
|
10
11
|
|
|
11
12
|
## Requirements
|
|
12
13
|
|
|
@@ -15,40 +16,58 @@ without hardcoded POSIX assumptions.
|
|
|
15
16
|
- Kimi Code CLI installed (`kimi.exe` on PATH or at `~/.kimi-code/bin/kimi.exe`),
|
|
16
17
|
authenticated (`~/.kimi-code/`)
|
|
17
18
|
|
|
18
|
-
##
|
|
19
|
+
## Quick start (from npm)
|
|
19
20
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
npm run build # compiles src/ -> dist/ (tests excluded)
|
|
23
|
-
```
|
|
21
|
+
You don't need to clone or build — the package is published on npm and your MCP
|
|
22
|
+
client launches it via `npx`.
|
|
24
23
|
|
|
25
|
-
|
|
24
|
+
**Claude Code (one command):**
|
|
26
25
|
|
|
27
26
|
```bash
|
|
28
|
-
|
|
29
|
-
npm run typecheck # tsc --noEmit (incl. tests)
|
|
30
|
-
npm run dev # run the server from source via tsx
|
|
27
|
+
claude mcp add kimi-code -- npx -y ladder-mcp
|
|
31
28
|
```
|
|
32
29
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
The server speaks MCP over stdio. Point your MCP client at the built entry:
|
|
30
|
+
**Or add it manually to your MCP config:**
|
|
36
31
|
|
|
37
32
|
```jsonc
|
|
38
|
-
// e.g. Claude Code MCP config
|
|
39
33
|
{
|
|
40
34
|
"mcpServers": {
|
|
41
35
|
"kimi-code": {
|
|
42
|
-
"command": "
|
|
43
|
-
"args": ["
|
|
36
|
+
"command": "npx",
|
|
37
|
+
"args": ["-y", "ladder-mcp"]
|
|
44
38
|
}
|
|
45
39
|
}
|
|
46
40
|
}
|
|
47
41
|
```
|
|
48
42
|
|
|
43
|
+
Then in Claude Code run `/mcp` (should show `kimi-code: connected`) and call
|
|
44
|
+
`kimi_status` to confirm the environment is detected.
|
|
45
|
+
|
|
46
|
+
> The server speaks MCP over **stdio**: it is launched and managed by the client,
|
|
47
|
+
> not run by hand. Running `npx ladder-mcp` directly will appear to "hang" — that
|
|
48
|
+
> is the server correctly waiting for a client. Exit with Ctrl+C.
|
|
49
|
+
|
|
50
|
+
Prefer a global install? `npm install -g ladder-mcp`, then use `ladder-mcp` as the
|
|
51
|
+
command instead of `npx -y ladder-mcp`.
|
|
52
|
+
|
|
49
53
|
To let Kimi Code itself host this server, use the `kimi_generate_mcp_config`
|
|
50
54
|
tool to produce/merge a `.kimi-code/mcp.json` entry.
|
|
51
55
|
|
|
56
|
+
## Build from source (contributors)
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
npm install
|
|
60
|
+
npm run build # compiles src/ -> dist/ (tests excluded)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Quick checks:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
npm test # vitest (50 tests)
|
|
67
|
+
npm run typecheck # tsc --noEmit (incl. tests)
|
|
68
|
+
npm run dev # run the server from source via tsx
|
|
69
|
+
```
|
|
70
|
+
|
|
52
71
|
## Tools
|
|
53
72
|
|
|
54
73
|
**Core (v1)** — `kimi_analyze`, `kimi_query`, `kimi_verify`, `kimi_resume`,
|
package/dist/environment.js
CHANGED
|
@@ -126,7 +126,10 @@ export function isAuthenticated(options) {
|
|
|
126
126
|
return Boolean(data.access_token || data.refresh_token || data.id_token || data.token);
|
|
127
127
|
}
|
|
128
128
|
catch {
|
|
129
|
-
|
|
129
|
+
// A corrupt or unreadable credentials file is NOT proof of authentication.
|
|
130
|
+
// Returning true here produced a false-positive "authenticated" status that
|
|
131
|
+
// misled diagnostics (NFR-5). Fail closed.
|
|
132
|
+
return false;
|
|
130
133
|
}
|
|
131
134
|
}
|
|
132
135
|
export function isKimiInstalled(options) {
|
package/dist/index.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
3
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
4
|
import { z } from 'zod';
|
|
5
|
+
import { maxChars, validateWorkDir } from './input-validation.js';
|
|
5
6
|
import { getKimiStatus } from './environment.js';
|
|
6
7
|
import { runKimiApi, isApiConfigured } from './kimi-api.js';
|
|
7
8
|
import { runKimi } from './kimi-runner.js';
|
|
@@ -13,9 +14,8 @@ import { generateMcpConfig } from './kimi-mcp-config.js';
|
|
|
13
14
|
import { buildBudgetProbeGuide, getDesktopStatus } from './desktop-work.js';
|
|
14
15
|
const server = new McpServer({
|
|
15
16
|
name: 'kimi-code',
|
|
16
|
-
version: '1.0.
|
|
17
|
+
version: '1.0.2',
|
|
17
18
|
});
|
|
18
|
-
const DEFAULT_MAX_OUTPUT_CHARS = 60_000;
|
|
19
19
|
const FORMAT_INSTRUCTIONS = {
|
|
20
20
|
summary: `
|
|
21
21
|
OUTPUT FORMAT CONSTRAINTS:
|
|
@@ -42,9 +42,6 @@ IMPORTANT: Your response will be consumed by another AI model with limited conte
|
|
|
42
42
|
function wrapPrompt(prompt, detailLevel) {
|
|
43
43
|
return `${prompt}\n${FORMAT_INSTRUCTIONS[detailLevel]}\n${AI_CONSUMER_NOTICE}`;
|
|
44
44
|
}
|
|
45
|
-
function maxChars(maxOutputTokens) {
|
|
46
|
-
return maxOutputTokens ? maxOutputTokens * 4 : DEFAULT_MAX_OUTPUT_CHARS;
|
|
47
|
-
}
|
|
48
45
|
function textResponse(text, isError = false) {
|
|
49
46
|
return { content: [{ type: 'text', text }], isError };
|
|
50
47
|
}
|
|
@@ -60,6 +57,9 @@ server.tool('kimi_analyze', 'Send a prompt to Kimi Code for codebase analysis. D
|
|
|
60
57
|
max_output_tokens: z.number().optional(),
|
|
61
58
|
include_thinking: z.boolean().optional(),
|
|
62
59
|
}, async ({ prompt, work_dir, session_id, new_session, detail_level, max_output_tokens, include_thinking }) => {
|
|
60
|
+
const workDirError = validateWorkDir(work_dir);
|
|
61
|
+
if (workDirError)
|
|
62
|
+
return textResponse(`Error: ${workDirError}`, true);
|
|
63
63
|
const result = await runKimi({
|
|
64
64
|
prompt: wrapPrompt(prompt, detail_level ?? 'normal'),
|
|
65
65
|
workDir: work_dir,
|
|
@@ -107,6 +107,9 @@ server.tool('kimi_resume', 'Resume an explicit Kimi Code session with a new prom
|
|
|
107
107
|
max_output_tokens: z.number().optional(),
|
|
108
108
|
include_thinking: z.boolean().optional(),
|
|
109
109
|
}, async ({ session_id, prompt, work_dir, detail_level, max_output_tokens, include_thinking }) => {
|
|
110
|
+
const workDirError = validateWorkDir(work_dir);
|
|
111
|
+
if (workDirError)
|
|
112
|
+
return textResponse(`Error: ${workDirError}`, true);
|
|
110
113
|
const result = await runKimi({
|
|
111
114
|
prompt: wrapPrompt(prompt, detail_level ?? 'normal'),
|
|
112
115
|
workDir: work_dir,
|
|
@@ -219,8 +222,11 @@ server.tool('kimi_chat', 'Send a prompt through Kimi ACP over stdio. Set backgro
|
|
|
219
222
|
});
|
|
220
223
|
return textResponse(JSON.stringify(result, null, 2), !result.ok);
|
|
221
224
|
});
|
|
222
|
-
server.tool('kimi_acp_sessions', 'List Kimi ACP sessions through `session/list`.', {
|
|
223
|
-
|
|
225
|
+
server.tool('kimi_acp_sessions', 'List Kimi ACP sessions through `session/list`.', {
|
|
226
|
+
limit: z.number().int().positive().optional(),
|
|
227
|
+
work_dir: z.string().optional(),
|
|
228
|
+
}, async ({ limit, work_dir }) => {
|
|
229
|
+
const result = await listAcpSessions({ limit, workDir: work_dir });
|
|
224
230
|
return textResponse(result.ok ? result.text : `Error: ${result.error}`, !result.ok);
|
|
225
231
|
});
|
|
226
232
|
server.tool('kimi_cancel', 'Cancel an active Ladder task by task_id or a Kimi ACP session by session_id.', {
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
export const DEFAULT_MAX_OUTPUT_CHARS = 60_000;
|
|
4
|
+
// Convert an untrusted max_output_tokens into a character budget. NaN/Infinity/zero/
|
|
5
|
+
// negative/non-integer would otherwise produce a nonsensical budget (e.g. NaN) handed
|
|
6
|
+
// to truncation, so clamp those to the default.
|
|
7
|
+
export function maxChars(maxOutputTokens) {
|
|
8
|
+
if (typeof maxOutputTokens !== 'number' || !Number.isFinite(maxOutputTokens) || maxOutputTokens <= 0) {
|
|
9
|
+
return DEFAULT_MAX_OUTPUT_CHARS;
|
|
10
|
+
}
|
|
11
|
+
return Math.floor(maxOutputTokens) * 4;
|
|
12
|
+
}
|
|
13
|
+
// Reject a work_dir before spawning Kimi: a relative or non-existent directory would
|
|
14
|
+
// otherwise be passed to the CLI as cwd and fail opaquely. Returns an error string when
|
|
15
|
+
// invalid, or undefined when the path is an existing absolute directory.
|
|
16
|
+
export function validateWorkDir(workDir) {
|
|
17
|
+
if (!path.isAbsolute(workDir))
|
|
18
|
+
return 'work_dir must be an absolute path.';
|
|
19
|
+
let stat;
|
|
20
|
+
try {
|
|
21
|
+
stat = fs.statSync(workDir);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return `work_dir does not exist: ${workDir}`;
|
|
25
|
+
}
|
|
26
|
+
if (!stat.isDirectory())
|
|
27
|
+
return `work_dir is not a directory: ${workDir}`;
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
package/dist/kimi-mcp-config.js
CHANGED
|
@@ -44,6 +44,19 @@ function assertWritableProjectTarget(projectDir) {
|
|
|
44
44
|
throw new Error('Refusing to write MCP config under read-only kimi-code-mcp reference tree.');
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
|
+
const ALLOWED_BARE_COMMANDS = new Set(['node', 'npx']);
|
|
48
|
+
// Constrain the launcher written into mcp.json. Without this, an arbitrary `command`
|
|
49
|
+
// (e.g. "powershell") would be persisted and later executed when Kimi loads the
|
|
50
|
+
// server config. Allow only the known Node launchers by bare name, or an absolute
|
|
51
|
+
// path to an existing file (a vetted local binary).
|
|
52
|
+
function assertSafeCommand(command) {
|
|
53
|
+
const value = command.trim();
|
|
54
|
+
if (ALLOWED_BARE_COMMANDS.has(value))
|
|
55
|
+
return;
|
|
56
|
+
if (path.isAbsolute(value) && fs.existsSync(value) && fs.statSync(value).isFile())
|
|
57
|
+
return;
|
|
58
|
+
throw new Error(`command must be one of [${[...ALLOWED_BARE_COMMANDS].join(', ')}] or an absolute path to an existing file; got "${command}".`);
|
|
59
|
+
}
|
|
47
60
|
function readMcpServers(existing) {
|
|
48
61
|
const value = existing.mcpServers;
|
|
49
62
|
if (value === undefined || value === null)
|
|
@@ -66,8 +79,10 @@ export function generateMcpConfig(options = {}) {
|
|
|
66
79
|
throw new Error('server_name must contain only letters, digits, underscores, and hyphens.');
|
|
67
80
|
}
|
|
68
81
|
const defaults = defaultServerCommand();
|
|
82
|
+
const command = options.command ?? defaults.command;
|
|
83
|
+
assertSafeCommand(command);
|
|
69
84
|
const serverConfig = {
|
|
70
|
-
command
|
|
85
|
+
command,
|
|
71
86
|
args: options.args ?? defaults.args,
|
|
72
87
|
env: {},
|
|
73
88
|
};
|
package/dist/kimi-runner.js
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import * as path from 'node:path';
|
|
3
3
|
import { buildKimiEnv, resolveKimiPaths } from './environment.js';
|
|
4
|
+
const MAX_CAPTURE_CHARS = 16 * 1024 * 1024;
|
|
5
|
+
// Append to a captured stream while enforcing a hard ceiling so a runaway CLI cannot
|
|
6
|
+
// grow the buffer without bound. Once the cap is reached, further data is dropped (the
|
|
7
|
+
// stream is still drained by Node, just not stored).
|
|
8
|
+
export function appendCapped(current, addition, max) {
|
|
9
|
+
if (current.length >= max)
|
|
10
|
+
return current;
|
|
11
|
+
const next = current + addition;
|
|
12
|
+
return next.length > max ? next.slice(0, max) : next;
|
|
13
|
+
}
|
|
4
14
|
export function buildKimiArgs(config) {
|
|
5
15
|
const args = ['-p', config.prompt, '--output-format', 'stream-json'];
|
|
6
16
|
if (config.sessionId) {
|
|
@@ -120,8 +130,8 @@ export function runKimi(config) {
|
|
|
120
130
|
killProcessTree(proc.pid);
|
|
121
131
|
finish({ ok: false, text: '', error: `Kimi timed out after ${Math.round(timeoutMs / 1000)}s` });
|
|
122
132
|
}, timeoutMs);
|
|
123
|
-
proc.stdout?.on('data', (chunk) => { stdout
|
|
124
|
-
proc.stderr?.on('data', (chunk) => { stderr
|
|
133
|
+
proc.stdout?.on('data', (chunk) => { stdout = appendCapped(stdout, chunk.toString('utf-8'), MAX_CAPTURE_CHARS); });
|
|
134
|
+
proc.stderr?.on('data', (chunk) => { stderr = appendCapped(stderr, chunk.toString('utf-8'), MAX_CAPTURE_CHARS); });
|
|
125
135
|
proc.on('error', (err) => {
|
|
126
136
|
finish({ ok: false, text: '', error: err instanceof Error ? err.message : String(err) });
|
|
127
137
|
});
|
package/dist/transports/acp.js
CHANGED
|
@@ -153,7 +153,11 @@ function extractTextDeep(value) {
|
|
|
153
153
|
return [...direct, ...nested];
|
|
154
154
|
}
|
|
155
155
|
export function extractAcpText(value) {
|
|
156
|
-
|
|
156
|
+
// Concatenate text fragments as-is. Kimi streams the answer as many small
|
|
157
|
+
// agent_message_chunk tokens (e.g. "Two", " plus", " two"); trimming each
|
|
158
|
+
// fragment or joining on newlines would drop the spaces between tokens and
|
|
159
|
+
// run words together. Callers trim the final assembled string.
|
|
160
|
+
return extractTextDeep(value).join('');
|
|
157
161
|
}
|
|
158
162
|
// Keep the trailing portion of `text` within a UTF-8 *byte* budget. Slicing by
|
|
159
163
|
// String length (UTF-16 code units) under-counts multibyte characters, so the
|
|
@@ -217,7 +221,7 @@ export class AcpClient extends EventEmitter {
|
|
|
217
221
|
async initialize() {
|
|
218
222
|
return this.request('initialize', {
|
|
219
223
|
protocolVersion: 1,
|
|
220
|
-
clientInfo: { name: 'ladder-mcp', version: '1.0.
|
|
224
|
+
clientInfo: { name: 'ladder-mcp', version: '1.0.2' },
|
|
221
225
|
capabilities: {},
|
|
222
226
|
}, 30_000);
|
|
223
227
|
}
|
|
@@ -357,7 +361,9 @@ export async function runAcpPrompt(options) {
|
|
|
357
361
|
sessionId = extractSessionId(await client.newSession(options.workDir));
|
|
358
362
|
}
|
|
359
363
|
const result = await client.prompt(sessionId, options.prompt, options.workDir);
|
|
360
|
-
|
|
364
|
+
// extractAcpText no longer trims fragments (to preserve inter-token spaces),
|
|
365
|
+
// so trim the fully assembled string here. getUpdateText already trims.
|
|
366
|
+
let text = extractAcpText(result).trim() || client.getUpdateText();
|
|
361
367
|
if (!text) {
|
|
362
368
|
try {
|
|
363
369
|
text = JSON.stringify(result, null, 2);
|
|
@@ -385,11 +391,28 @@ export async function runAcpPrompt(options) {
|
|
|
385
391
|
client.close();
|
|
386
392
|
}
|
|
387
393
|
}
|
|
388
|
-
|
|
394
|
+
const DEFAULT_ACP_SESSION_LIMIT = 20;
|
|
395
|
+
function normalizePath(value) {
|
|
396
|
+
return value.replace(/\\/g, '/').replace(/\/+$/, '').toLowerCase();
|
|
397
|
+
}
|
|
398
|
+
export async function listAcpSessions(options = {}) {
|
|
389
399
|
const client = new AcpClient(60_000);
|
|
390
400
|
try {
|
|
391
401
|
await client.initialize();
|
|
392
402
|
const result = await client.listSessions();
|
|
403
|
+
// Filter by working directory and cap the count for parity with kimi_list_sessions;
|
|
404
|
+
// session/list otherwise returns every ACP session across all projects in one blob.
|
|
405
|
+
const sessions = result?.sessions;
|
|
406
|
+
if (Array.isArray(sessions)) {
|
|
407
|
+
let filtered = sessions;
|
|
408
|
+
if (options.workDir) {
|
|
409
|
+
const target = normalizePath(options.workDir);
|
|
410
|
+
filtered = filtered.filter((s) => typeof s?.cwd === 'string' && normalizePath(s.cwd) === target);
|
|
411
|
+
}
|
|
412
|
+
const limit = options.limit ?? DEFAULT_ACP_SESSION_LIMIT;
|
|
413
|
+
const limited = limit > 0 ? filtered.slice(0, limit) : filtered;
|
|
414
|
+
return { ok: true, text: JSON.stringify({ sessions: limited, total: filtered.length }, null, 2) };
|
|
415
|
+
}
|
|
393
416
|
return { ok: true, text: JSON.stringify(result, null, 2) };
|
|
394
417
|
}
|
|
395
418
|
catch (error) {
|
|
@@ -3,6 +3,7 @@ import * as fs from 'node:fs';
|
|
|
3
3
|
import * as path from 'node:path';
|
|
4
4
|
import { promisify } from 'node:util';
|
|
5
5
|
import { buildKimiEnv, getKimiStatus, resolveKimiPaths } from '../environment.js';
|
|
6
|
+
import { listSessions } from '../session-store.js';
|
|
6
7
|
const execFileAsync = promisify(execFile);
|
|
7
8
|
const ADMIN_TIMEOUT_MS = 30_000;
|
|
8
9
|
// Quote a single argument for the human-readable preview command. This string is
|
|
@@ -76,8 +77,12 @@ export function buildExportArgs(options) {
|
|
|
76
77
|
if (options.sessionId)
|
|
77
78
|
args.push(options.sessionId);
|
|
78
79
|
args.push('-o', options.outputPath);
|
|
79
|
-
|
|
80
|
-
|
|
80
|
+
// Always auto-confirm. `kimi export` prompts "Export previous session …? [Y/n]"
|
|
81
|
+
// even for a brand-new path; since we close stdin, that prompt would get EOF and
|
|
82
|
+
// the export would silently no-op while still exiting 0. Overwrite safety is
|
|
83
|
+
// enforced by our own pre-check (assertSafeOutputPath + statSync + overwrite_existing),
|
|
84
|
+
// so unconditionally passing -y is safe.
|
|
85
|
+
args.push('-y');
|
|
81
86
|
if (options.includeGlobalLog !== true)
|
|
82
87
|
args.push('--no-include-global-log');
|
|
83
88
|
return args;
|
|
@@ -179,6 +184,18 @@ export async function exportKimiSession(options) {
|
|
|
179
184
|
error: error instanceof Error ? error.message : String(error),
|
|
180
185
|
};
|
|
181
186
|
}
|
|
187
|
+
// Resolve an explicit session id ourselves when the caller omitted one. `kimi export`
|
|
188
|
+
// otherwise defaults to "the most recent session" and asks "Export previous session …?
|
|
189
|
+
// [Y/n]" — a prompt that `-y` does NOT suppress in Kimi CLI 0.20.1, so with stdin closed
|
|
190
|
+
// it hangs/aborts and writes nothing. Passing an explicit id skips that prompt entirely.
|
|
191
|
+
let sessionId = options.sessionId;
|
|
192
|
+
if (!sessionId) {
|
|
193
|
+
const recent = listSessions({ limit: 1 })[0];
|
|
194
|
+
if (!recent) {
|
|
195
|
+
return { ok: false, stdout: '', stderr: '', error: 'No Kimi session found to export. Provide an explicit session_id.' };
|
|
196
|
+
}
|
|
197
|
+
sessionId = recent.id;
|
|
198
|
+
}
|
|
182
199
|
const outputPath = path.resolve(options.outputPath);
|
|
183
200
|
let existingStat;
|
|
184
201
|
try {
|
|
@@ -207,7 +224,18 @@ export async function exportKimiSession(options) {
|
|
|
207
224
|
};
|
|
208
225
|
}
|
|
209
226
|
}
|
|
210
|
-
|
|
227
|
+
const result = await runKimiCommand(buildExportArgs({ ...options, sessionId, outputPath }), 120_000);
|
|
228
|
+
// The CLI can exit 0 without producing the archive (e.g. a declined/aborted prompt).
|
|
229
|
+
// Verify the file actually exists so we never report success for a no-op export.
|
|
230
|
+
if (result.ok && !fs.existsSync(outputPath)) {
|
|
231
|
+
return {
|
|
232
|
+
ok: false,
|
|
233
|
+
stdout: result.stdout,
|
|
234
|
+
stderr: result.stderr,
|
|
235
|
+
error: 'Kimi export exited without creating the output file. No archive was written.',
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
return result;
|
|
211
239
|
}
|
|
212
240
|
export function visualizeSession(options) {
|
|
213
241
|
const binary = requireBinary();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ladder-mcp",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Windows-first MCP bridge for Kimi Code CLI v24",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
"build": "tsc -p tsconfig.build.json",
|
|
20
20
|
"start": "node dist/index.js",
|
|
21
21
|
"test": "vitest run",
|
|
22
|
-
"typecheck": "tsc --noEmit"
|
|
22
|
+
"typecheck": "tsc --noEmit",
|
|
23
|
+
"prepublishOnly": "npm run build && npm test"
|
|
23
24
|
},
|
|
24
25
|
"engines": {
|
|
25
26
|
"node": ">=18.0.0"
|