ladder-mcp 1.1.0 → 1.1.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 +27 -0
- package/README.md +21 -15
- package/dist/index.js +5 -3
- package/dist/kimi-runner.js +73 -1
- package/dist/transports/acp.js +9 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,33 @@ 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.1.2] - 2026-06-28
|
|
8
|
+
|
|
9
|
+
Live-progress and cancellation reliability for `kimi_code`.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- The CLI transport now surfaces the running action: `stream-json` `tool_calls`
|
|
14
|
+
records emit immediate `tool_call` progress events (e.g. `Read src/types.ts`)
|
|
15
|
+
and `plan` records emit `plan` events, matching the ACP transport. A CLI job
|
|
16
|
+
no longer looks idle while it works.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- Cancelling a `kimi_code` call now actually kills the underlying `kimi.exe`
|
|
21
|
+
process in every mode (foreground CLI, background CLI, foreground ACP).
|
|
22
|
+
Previously only background ACP honored cancellation; the others leaked the
|
|
23
|
+
child until the timeout fired. `runKimi` accepts an `AbortSignal`, skips
|
|
24
|
+
spawning when already aborted, and kills the process tree on abort.
|
|
25
|
+
- The ACP client is hardened against a `close()`-before-`start()` race, and an
|
|
26
|
+
aborted ACP run now reports `Kimi cancelled` instead of a raw exit code.
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
|
|
30
|
+
- `kimi_code`'s `transport` and `background` parameter descriptions now explain
|
|
31
|
+
the cli/acp trade-off and where to read the full progress log, so agents pick
|
|
32
|
+
a mode deliberately.
|
|
33
|
+
|
|
7
34
|
## [1.1.0] - 2026-06-28
|
|
8
35
|
|
|
9
36
|
Breaking tool-surface redesign: the MCP tool list is now an intent-first set of
|
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ 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.
|
|
9
|
+
> Status: **v1.1.2** ([npm](https://www.npmjs.com/package/ladder-mcp)).
|
|
10
10
|
> Supported platform is **Windows 11 only**.
|
|
11
11
|
|
|
12
12
|
## Requirements
|
|
@@ -19,7 +19,7 @@ without hardcoded POSIX assumptions.
|
|
|
19
19
|
## Quick start (from npm)
|
|
20
20
|
|
|
21
21
|
You don't need to clone or build — the package is published on npm and your MCP
|
|
22
|
-
client launches it via `npx
|
|
22
|
+
client launches it via `npx`, or you can install the package directly.
|
|
23
23
|
|
|
24
24
|
**Claude Code (one command):**
|
|
25
25
|
|
|
@@ -50,7 +50,16 @@ Then in Claude Code run `/mcp` (should show `kimi-code: connected`) and call
|
|
|
50
50
|
Prefer a global install? `npm install -g ladder-mcp`, then use `ladder-mcp` as the
|
|
51
51
|
command instead of `npx -y ladder-mcp`.
|
|
52
52
|
|
|
53
|
-
|
|
53
|
+
Or install locally into your project:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
npm install ladder-mcp
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Then point your MCP config at `./node_modules/.bin/ladder-mcp` (or use
|
|
60
|
+
`npx -y ladder-mcp`, which resolves the locally installed copy when available).
|
|
61
|
+
|
|
62
|
+
To let Kimi Code itself host this server, use the `kimi_setup`
|
|
54
63
|
tool to produce/merge a `.kimi-code/mcp.json` entry.
|
|
55
64
|
|
|
56
65
|
## Build from source (contributors)
|
|
@@ -63,26 +72,23 @@ npm run build # compiles src/ -> dist/ (tests excluded)
|
|
|
63
72
|
Quick checks:
|
|
64
73
|
|
|
65
74
|
```bash
|
|
66
|
-
npm test # vitest (
|
|
75
|
+
npm test # vitest (153 tests)
|
|
67
76
|
npm run typecheck # tsc --noEmit (incl. tests)
|
|
68
77
|
npm run dev # run the server from source via tsx
|
|
69
78
|
```
|
|
70
79
|
|
|
71
80
|
## Tools
|
|
72
81
|
|
|
73
|
-
**Core (v1)** — `
|
|
74
|
-
`
|
|
75
|
-
|
|
76
|
-
**CLI admin & capabilities** — `kimi_capabilities`, `kimi_doctor`,
|
|
77
|
-
`kimi_provider_list`, `kimi_export_session`, `kimi_visualize_session`
|
|
78
|
-
|
|
79
|
-
**ACP (chat over stdio)** — `kimi_chat`, `kimi_acp_sessions`, `kimi_cancel`
|
|
82
|
+
**Core (v1.1)** — `kimi_code`, `kimi_ask`, `kimi_sessions`, `kimi_tasks`,
|
|
83
|
+
`kimi_status`, `kimi_setup`
|
|
80
84
|
|
|
81
|
-
|
|
82
|
-
(set `background=true`
|
|
85
|
+
`kimi_code` supports both the native CLI transport (default) and the ACP
|
|
86
|
+
JSON-RPC transport (`transport: 'acp'`); set `background=true` to track long
|
|
87
|
+
work as a background task.
|
|
83
88
|
|
|
84
|
-
**
|
|
85
|
-
`kimi_desktop_status`, `kimi_budget_probe
|
|
89
|
+
**Experimental (off by default)** — `kimi_export_session`,
|
|
90
|
+
`kimi_visualize_session`, `kimi_desktop_status`, `kimi_budget_probe`. Enable
|
|
91
|
+
with the environment variable `LADDER_EXPERIMENTAL=1`.
|
|
86
92
|
|
|
87
93
|
## Safety boundaries
|
|
88
94
|
|
package/dist/index.js
CHANGED
|
@@ -62,8 +62,8 @@ server.tool('kimi_code', 'Agentic work in a repository: analyze and edit files.
|
|
|
62
62
|
session_id: z.string().optional().describe('Explicit Kimi session id to resume.'),
|
|
63
63
|
new_session: z.boolean().optional().describe('Start fresh instead of continuing the last session. Default: false.'),
|
|
64
64
|
edit: z.boolean().optional().describe('Allow file modifications. Default: false (analysis-only intent).'),
|
|
65
|
-
background: z.boolean().optional().describe('Track as a long-running background task.'),
|
|
66
|
-
transport: z.enum(['cli', 'acp']).optional().describe("
|
|
65
|
+
background: z.boolean().optional().describe('Track as a long-running background task. Every progress event (including each action) is appended to the task log, readable via kimi_tasks — the full accumulating transcript, unlike the single overwriting live-progress line of a foreground call.'),
|
|
66
|
+
transport: z.enum(['cli', 'acp']).optional().describe("How the server drives Kimi. Both edit files and resume sessions; they differ in robustness and live-progress detail. 'cli' (default): one-shot process, most robust on Windows, but live progress is coarse — only streaming-output volume, no per-action lines. 'acp': persistent JSON-RPC session that emits granular live progress (tool calls, plan steps) and supports interactive permission prompts, but is heavier and more fragile. Prefer 'cli' for plain codegen/analysis; choose 'acp' when you need to watch each action live or handle mid-run prompts."),
|
|
67
67
|
session_mode: z.enum(['new', 'load', 'resume']).optional().describe("ACP session mode when transport='acp'. Default: inferred from session_id/new_session."),
|
|
68
68
|
detail_level: z.enum(['summary', 'normal', 'detailed']).optional(),
|
|
69
69
|
max_output_tokens: z.number().optional(),
|
|
@@ -89,6 +89,7 @@ server.tool('kimi_code', 'Agentic work in a repository: analyze and edit files.
|
|
|
89
89
|
maxOutputChars: maxChars(max_output_tokens),
|
|
90
90
|
includeThinking: includeThinkingValue,
|
|
91
91
|
onProgress: appendReporter(append),
|
|
92
|
+
signal: _signal,
|
|
92
93
|
});
|
|
93
94
|
if (!result.ok)
|
|
94
95
|
throw new Error(result.error ?? 'CLI code task failed');
|
|
@@ -106,6 +107,7 @@ server.tool('kimi_code', 'Agentic work in a repository: analyze and edit files.
|
|
|
106
107
|
maxOutputChars: maxChars(max_output_tokens),
|
|
107
108
|
includeThinking: includeThinkingValue,
|
|
108
109
|
onProgress: createMcpProgressReporter(extra),
|
|
110
|
+
signal: extra?.signal,
|
|
109
111
|
});
|
|
110
112
|
if (!result.ok)
|
|
111
113
|
return textResponse(`Error: ${result.error}`, true);
|
|
@@ -135,7 +137,7 @@ server.tool('kimi_code', 'Agentic work in a repository: analyze and edit files.
|
|
|
135
137
|
});
|
|
136
138
|
return textResponse(JSON.stringify(task, null, 2));
|
|
137
139
|
}
|
|
138
|
-
const result = await runAcp(
|
|
140
|
+
const result = await runAcp(extra?.signal, createMcpProgressReporter(extra));
|
|
139
141
|
return textResponse(JSON.stringify(result, null, 2), !result.ok);
|
|
140
142
|
});
|
|
141
143
|
server.tool('kimi_ask', 'Stateless question or independent review. Text only, no repo, no edits.', {
|
package/dist/kimi-runner.js
CHANGED
|
@@ -52,6 +52,46 @@ function contentToText(content) {
|
|
|
52
52
|
return '[non-text]';
|
|
53
53
|
}).join('');
|
|
54
54
|
}
|
|
55
|
+
// The Kimi CLI emits tool calls on `role:assistant` records as a `tool_calls`
|
|
56
|
+
// array shaped like OpenAI function calls. Extract a concise "Action target"
|
|
57
|
+
// string so CLI runs can surface the current action as an immediate progress
|
|
58
|
+
// event, matching the ACP transport behavior.
|
|
59
|
+
function extractCliToolTarget(argsJson) {
|
|
60
|
+
if (!argsJson)
|
|
61
|
+
return undefined;
|
|
62
|
+
try {
|
|
63
|
+
const parsed = JSON.parse(argsJson);
|
|
64
|
+
const value = parsed.path ?? parsed.skill ?? parsed.command;
|
|
65
|
+
if (typeof value === 'string' && value.trim()) {
|
|
66
|
+
const trimmed = value.trim();
|
|
67
|
+
return trimmed.length > 80 ? `${trimmed.slice(0, 80)}…` : trimmed;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
// Ignore malformed argument JSON.
|
|
72
|
+
}
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
function formatCliToolCall(toolCall) {
|
|
76
|
+
if (!toolCall || typeof toolCall !== 'object')
|
|
77
|
+
return undefined;
|
|
78
|
+
const tc = toolCall;
|
|
79
|
+
let name;
|
|
80
|
+
let args;
|
|
81
|
+
if (tc.function && typeof tc.function === 'object') {
|
|
82
|
+
const fn = tc.function;
|
|
83
|
+
name = typeof fn.name === 'string' ? fn.name : undefined;
|
|
84
|
+
args = typeof fn.arguments === 'string' ? fn.arguments : undefined;
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
name = typeof tc.name === 'string' ? tc.name : undefined;
|
|
88
|
+
args = typeof tc.arguments === 'string' ? tc.arguments : undefined;
|
|
89
|
+
}
|
|
90
|
+
if (!name)
|
|
91
|
+
return undefined;
|
|
92
|
+
const target = extractCliToolTarget(args);
|
|
93
|
+
return target ? `${name} ${target}` : name;
|
|
94
|
+
}
|
|
55
95
|
function extractResumeHint(record) {
|
|
56
96
|
if (record.role !== 'meta')
|
|
57
97
|
return undefined;
|
|
@@ -166,6 +206,13 @@ export function runKimi(config) {
|
|
|
166
206
|
error: 'Kimi CLI binary was not found on PATH or at ~/.kimi-code/bin/kimi.exe.',
|
|
167
207
|
});
|
|
168
208
|
}
|
|
209
|
+
if (config.signal?.aborted) {
|
|
210
|
+
return Promise.resolve({
|
|
211
|
+
ok: false,
|
|
212
|
+
text: '',
|
|
213
|
+
error: 'Kimi cancelled',
|
|
214
|
+
});
|
|
215
|
+
}
|
|
169
216
|
return new Promise((resolve) => {
|
|
170
217
|
let settled = false;
|
|
171
218
|
const proc = spawn(paths.binaryPath, buildKimiArgs(config), {
|
|
@@ -180,6 +227,7 @@ export function runKimi(config) {
|
|
|
180
227
|
let streamBuffer = '';
|
|
181
228
|
const coalescer = config.onProgress ? createStreamCoalescer(config.onProgress) : undefined;
|
|
182
229
|
const watchdog = config.onProgress ? createStallWatchdog(config.onProgress) : undefined;
|
|
230
|
+
let onAbort;
|
|
183
231
|
const finish = (result) => {
|
|
184
232
|
if (settled)
|
|
185
233
|
return;
|
|
@@ -188,8 +236,19 @@ export function runKimi(config) {
|
|
|
188
236
|
coalescer?.flush();
|
|
189
237
|
coalescer?.stop();
|
|
190
238
|
watchdog?.stop();
|
|
239
|
+
config.signal?.removeEventListener('abort', onAbort);
|
|
191
240
|
resolve(result);
|
|
192
241
|
};
|
|
242
|
+
onAbort = () => {
|
|
243
|
+
killProcessTree(proc.pid);
|
|
244
|
+
finish({ ok: false, text: '', error: 'Kimi cancelled' });
|
|
245
|
+
};
|
|
246
|
+
config.signal?.addEventListener('abort', onAbort, { once: true });
|
|
247
|
+
// Cover the race where the signal is already aborted right after registration.
|
|
248
|
+
if (config.signal?.aborted) {
|
|
249
|
+
onAbort();
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
193
252
|
let timer = setTimeout(() => {
|
|
194
253
|
timedOut = true;
|
|
195
254
|
killProcessTree(proc.pid);
|
|
@@ -213,10 +272,23 @@ export function runKimi(config) {
|
|
|
213
272
|
continue;
|
|
214
273
|
try {
|
|
215
274
|
const record = JSON.parse(line);
|
|
216
|
-
if (record.
|
|
275
|
+
if (record.type === 'plan') {
|
|
276
|
+
const text = contentToText(record.content).trim();
|
|
277
|
+
if (text)
|
|
278
|
+
config.onProgress(makeEvent('plan', text));
|
|
279
|
+
}
|
|
280
|
+
else if (record.role === 'assistant') {
|
|
217
281
|
const text = contentToText(record.content);
|
|
218
282
|
if (text.trim())
|
|
219
283
|
coalescer?.add(text);
|
|
284
|
+
const toolCalls = record.tool_calls;
|
|
285
|
+
if (Array.isArray(toolCalls)) {
|
|
286
|
+
for (const toolCall of toolCalls) {
|
|
287
|
+
const toolText = formatCliToolCall(toolCall);
|
|
288
|
+
if (toolText)
|
|
289
|
+
config.onProgress(makeEvent('tool_call', toolText));
|
|
290
|
+
}
|
|
291
|
+
}
|
|
220
292
|
}
|
|
221
293
|
}
|
|
222
294
|
catch {
|
package/dist/transports/acp.js
CHANGED
|
@@ -232,6 +232,8 @@ export class AcpClient extends EventEmitter {
|
|
|
232
232
|
start() {
|
|
233
233
|
if (this.proc)
|
|
234
234
|
return;
|
|
235
|
+
if (this.closing)
|
|
236
|
+
throw new Error('ACP client is closing');
|
|
235
237
|
const binary = resolveKimiPaths().binaryPath;
|
|
236
238
|
if (!binary)
|
|
237
239
|
throw new Error('Kimi CLI binary was not found on PATH or at ~/.kimi-code/bin/kimi.exe.');
|
|
@@ -310,10 +312,11 @@ export class AcpClient extends EventEmitter {
|
|
|
310
312
|
return this.updates.join('').trim();
|
|
311
313
|
}
|
|
312
314
|
close() {
|
|
313
|
-
if (
|
|
315
|
+
if (this.closing)
|
|
314
316
|
return;
|
|
315
317
|
this.closing = true;
|
|
316
|
-
|
|
318
|
+
if (this.proc)
|
|
319
|
+
killProcessTree(this.proc.pid);
|
|
317
320
|
}
|
|
318
321
|
handleMessages(chunk) {
|
|
319
322
|
let messages;
|
|
@@ -665,7 +668,10 @@ export async function runAcpPrompt(options) {
|
|
|
665
668
|
return { ok: true, text: text || '(empty ACP response from Kimi)', sessionId, metadata };
|
|
666
669
|
}
|
|
667
670
|
catch (error) {
|
|
668
|
-
|
|
671
|
+
const message = options.signal?.aborted
|
|
672
|
+
? 'Kimi cancelled'
|
|
673
|
+
: error instanceof Error ? error.message : String(error);
|
|
674
|
+
return { ok: false, text: '', error: message };
|
|
669
675
|
}
|
|
670
676
|
finally {
|
|
671
677
|
if (onNotification)
|