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 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.0.1** ([npm](https://www.npmjs.com/package/ladder-mcp)).
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
- To let Kimi Code itself host this server, use the `kimi_generate_mcp_config`
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 (50 tests)
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)** — `kimi_analyze`, `kimi_query`, `kimi_verify`, `kimi_resume`,
74
- `kimi_list_sessions`, `kimi_status`
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
- **Background tasks** `kimi_task_status`, `kimi_task_output`, `kimi_task_cancel`
82
- (set `background=true` on `kimi_chat`)
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
- **Kimi-hosted config & desktop (read-only)** — `kimi_generate_mcp_config`,
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("Transport: 'cli' edits in-process; 'acp' uses JSON-RPC. Default: 'cli'."),
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(undefined, createMcpProgressReporter(extra));
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.', {
@@ -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.role === 'assistant') {
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 {
@@ -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 (!this.proc || this.closing)
315
+ if (this.closing)
314
316
  return;
315
317
  this.closing = true;
316
- killProcessTree(this.proc.pid);
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
- return { ok: false, text: '', error: error instanceof Error ? error.message : String(error) };
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)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ladder-mcp",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
4
4
  "description": "Windows-first MCP bridge for Kimi Code CLI v24",
5
5
  "license": "MIT",
6
6
  "type": "module",