ladder-mcp 1.1.2 → 1.1.4
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 +42 -0
- package/README.md +2 -2
- package/dist/index.js +1 -1
- package/dist/kimi-runner.js +26 -37
- package/dist/progress.js +71 -0
- package/dist/transports/acp.js +2 -40
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,48 @@ 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.4] - 2026-06-28
|
|
8
|
+
|
|
9
|
+
Make the live-progress line worth watching.
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- Live progress now shows a readable content **preview** of the streamed
|
|
14
|
+
`message`/`thought` text (whitespace collapsed, code-point-safe truncation with
|
|
15
|
+
an ellipsis) instead of a bare `(N chars)` counter.
|
|
16
|
+
- Progress emission is throttled to at most one update per ~1.2s, so the single
|
|
17
|
+
progress line stops flickering and each line stays readable. `tool_call`,
|
|
18
|
+
`tool_update`, and `plan` events remain immediate and flush any pending preview
|
|
19
|
+
first to preserve order.
|
|
20
|
+
- The two per-transport coalescers (CLI and ACP) are unified into one shared
|
|
21
|
+
`createProgressCoalescer` in `src/progress.ts`; the duplicated `COALESCE_*`
|
|
22
|
+
constants are gone.
|
|
23
|
+
|
|
24
|
+
### Notes
|
|
25
|
+
|
|
26
|
+
- Kimi CLI 0.20.1 does not stream thinking to stderr during `-p` runs, so live
|
|
27
|
+
`thought` progress is not available on the CLI transport (documented in code).
|
|
28
|
+
The ACP transport already emits `thought`, which now gets the preview treatment.
|
|
29
|
+
|
|
30
|
+
## [1.1.3] - 2026-06-28
|
|
31
|
+
|
|
32
|
+
Bugfix: analysis-mode `kimi_code` was broken against Kimi CLI 0.20.1.
|
|
33
|
+
|
|
34
|
+
### Fixed
|
|
35
|
+
|
|
36
|
+
- `kimi_code` without `edit: true` (the default analysis mode) crashed against
|
|
37
|
+
Kimi CLI 0.20.1 with `Cannot combine --prompt with --plan`. `buildKimiArgs` no
|
|
38
|
+
longer passes `--plan` in `-p` prompt mode (the CLI rejects it, along with
|
|
39
|
+
`--auto`/`-y`).
|
|
40
|
+
|
|
41
|
+
### Changed
|
|
42
|
+
|
|
43
|
+
- Because Kimi 0.20.1 has no `-p`-compatible read-only flag, the `edit: false`
|
|
44
|
+
analysis-only contract is now enforced at the prompt level: `runKimi` prepends
|
|
45
|
+
a read-only guard (`applyReadOnlyGuard`) when `edit` is not `true`. This is
|
|
46
|
+
advisory (the model is instructed not to edit) rather than a hard CLI
|
|
47
|
+
guarantee; the `edit` parameter description documents the change.
|
|
48
|
+
|
|
7
49
|
## [1.1.2] - 2026-06-28
|
|
8
50
|
|
|
9
51
|
Live-progress and cancellation reliability for `kimi_code`.
|
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.1.
|
|
9
|
+
> Status: **v1.1.4** ([npm](https://www.npmjs.com/package/ladder-mcp)).
|
|
10
10
|
> Supported platform is **Windows 11 only**.
|
|
11
11
|
|
|
12
12
|
## Requirements
|
|
@@ -72,7 +72,7 @@ npm run build # compiles src/ -> dist/ (tests excluded)
|
|
|
72
72
|
Quick checks:
|
|
73
73
|
|
|
74
74
|
```bash
|
|
75
|
-
npm test # vitest (
|
|
75
|
+
npm test # vitest (163 tests)
|
|
76
76
|
npm run typecheck # tsc --noEmit (incl. tests)
|
|
77
77
|
npm run dev # run the server from source via tsx
|
|
78
78
|
```
|
package/dist/index.js
CHANGED
|
@@ -61,7 +61,7 @@ server.tool('kimi_code', 'Agentic work in a repository: analyze and edit files.
|
|
|
61
61
|
work_dir: z.string().describe('Absolute path to the codebase root directory.'),
|
|
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
|
-
edit: z.boolean().optional().describe('Allow file modifications. Default: false (analysis-only intent).'),
|
|
64
|
+
edit: z.boolean().optional().describe('Allow file modifications. Default: false (analysis-only intent). On Kimi 0.20.1 this is prompt-enforced (advisory): when false/omitted the prompt is prefixed with a read-only guard, but the CLI itself provides no hard read-only flag for -p mode.'),
|
|
65
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
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."),
|
package/dist/kimi-runner.js
CHANGED
|
@@ -2,7 +2,7 @@ import { spawn } from 'node:child_process';
|
|
|
2
2
|
import * as path from 'node:path';
|
|
3
3
|
import { clampTimeout } from './transports/acp.js';
|
|
4
4
|
import { buildKimiEnv, resolveKimiPaths } from './environment.js';
|
|
5
|
-
import {
|
|
5
|
+
import { createProgressCoalescer, createStallWatchdog } from './progress.js';
|
|
6
6
|
const MAX_CAPTURE_CHARS = 16 * 1024 * 1024;
|
|
7
7
|
const MAX_FAILURE_STREAM_CHARS = 2_000;
|
|
8
8
|
// Append to a captured stream while enforcing a hard ceiling so a runaway CLI cannot
|
|
@@ -22,11 +22,21 @@ export function buildKimiArgs(config) {
|
|
|
22
22
|
else if (config.continueLast) {
|
|
23
23
|
args.push('-C');
|
|
24
24
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
// Kimi CLI 0.20.1 rejects combining --plan with -p/--prompt (non-interactive
|
|
26
|
+
// prompt mode), and the other restriction flags (--auto, -y) are also
|
|
27
|
+
// incompatible. For edit:false / omitted edit we therefore pass no restriction
|
|
28
|
+
// flag and rely on the prompt/system to avoid edits.
|
|
28
29
|
return args;
|
|
29
30
|
}
|
|
31
|
+
const READ_ONLY_GUARD = '[READ-ONLY ANALYSIS MODE] Do not create, modify, or delete any files, directories, or repository state. Only read, analyze, explain, and report.';
|
|
32
|
+
// Prepends a read-only guard to the prompt when edit mode is not explicitly
|
|
33
|
+
// enabled. Kimi CLI 0.20.1 has no -p-compatible read-only flag, so this is the
|
|
34
|
+
// only available enforcement mechanism for the analysis-only contract.
|
|
35
|
+
export function applyReadOnlyGuard(prompt, edit) {
|
|
36
|
+
if (edit === true)
|
|
37
|
+
return prompt;
|
|
38
|
+
return `${READ_ONLY_GUARD}\n\n${prompt}`;
|
|
39
|
+
}
|
|
30
40
|
function contentToText(content) {
|
|
31
41
|
if (typeof content === 'string')
|
|
32
42
|
return content;
|
|
@@ -156,32 +166,6 @@ export function truncateAtBoundary(text, maxChars) {
|
|
|
156
166
|
const cutPoint = Math.max(slice.lastIndexOf('\n## '), slice.lastIndexOf('\n\n'), Math.floor(maxChars * 0.8));
|
|
157
167
|
return `${slice.slice(0, cutPoint).trimEnd()}\n\n---\nOutput truncated (${text.length.toLocaleString()} chars exceeded ${maxChars.toLocaleString()} char budget). Use kimi_resume with the same session for follow-up questions.`;
|
|
158
168
|
}
|
|
159
|
-
const COALESCE_MS = 750;
|
|
160
|
-
const COALESCE_CHARS = 400;
|
|
161
|
-
function createStreamCoalescer(reporter) {
|
|
162
|
-
let count = 0;
|
|
163
|
-
let timer;
|
|
164
|
-
const flush = () => {
|
|
165
|
-
if (timer) {
|
|
166
|
-
clearTimeout(timer);
|
|
167
|
-
timer = undefined;
|
|
168
|
-
}
|
|
169
|
-
if (count > 0) {
|
|
170
|
-
reporter(makeEvent('message', `streaming output… (${count} chars)`));
|
|
171
|
-
count = 0;
|
|
172
|
-
}
|
|
173
|
-
};
|
|
174
|
-
const add = (text) => {
|
|
175
|
-
count += text.length;
|
|
176
|
-
if (count >= COALESCE_CHARS) {
|
|
177
|
-
flush();
|
|
178
|
-
}
|
|
179
|
-
else if (!timer) {
|
|
180
|
-
timer = setTimeout(flush, COALESCE_MS);
|
|
181
|
-
}
|
|
182
|
-
};
|
|
183
|
-
return { add, flush, stop: flush };
|
|
184
|
-
}
|
|
185
169
|
function killProcessTree(pid) {
|
|
186
170
|
if (!pid)
|
|
187
171
|
return;
|
|
@@ -213,9 +197,10 @@ export function runKimi(config) {
|
|
|
213
197
|
error: 'Kimi cancelled',
|
|
214
198
|
});
|
|
215
199
|
}
|
|
200
|
+
const guardedPrompt = applyReadOnlyGuard(config.prompt, config.edit);
|
|
216
201
|
return new Promise((resolve) => {
|
|
217
202
|
let settled = false;
|
|
218
|
-
const proc = spawn(paths.binaryPath, buildKimiArgs(config), {
|
|
203
|
+
const proc = spawn(paths.binaryPath, buildKimiArgs({ ...config, prompt: guardedPrompt }), {
|
|
219
204
|
env: buildKimiEnv(),
|
|
220
205
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
221
206
|
windowsHide: true,
|
|
@@ -225,7 +210,7 @@ export function runKimi(config) {
|
|
|
225
210
|
let stderr = '';
|
|
226
211
|
let timedOut = false;
|
|
227
212
|
let streamBuffer = '';
|
|
228
|
-
const coalescer = config.onProgress ?
|
|
213
|
+
const coalescer = config.onProgress ? createProgressCoalescer(config.onProgress) : undefined;
|
|
229
214
|
const watchdog = config.onProgress ? createStallWatchdog(config.onProgress) : undefined;
|
|
230
215
|
let onAbort;
|
|
231
216
|
const finish = (result) => {
|
|
@@ -233,7 +218,6 @@ export function runKimi(config) {
|
|
|
233
218
|
return;
|
|
234
219
|
settled = true;
|
|
235
220
|
clearTimeout(timer);
|
|
236
|
-
coalescer?.flush();
|
|
237
221
|
coalescer?.stop();
|
|
238
222
|
watchdog?.stop();
|
|
239
223
|
config.signal?.removeEventListener('abort', onAbort);
|
|
@@ -275,18 +259,18 @@ export function runKimi(config) {
|
|
|
275
259
|
if (record.type === 'plan') {
|
|
276
260
|
const text = contentToText(record.content).trim();
|
|
277
261
|
if (text)
|
|
278
|
-
|
|
262
|
+
coalescer?.add('plan', text);
|
|
279
263
|
}
|
|
280
264
|
else if (record.role === 'assistant') {
|
|
281
265
|
const text = contentToText(record.content);
|
|
282
266
|
if (text.trim())
|
|
283
|
-
coalescer?.add(text);
|
|
267
|
+
coalescer?.add('message', text);
|
|
284
268
|
const toolCalls = record.tool_calls;
|
|
285
269
|
if (Array.isArray(toolCalls)) {
|
|
286
270
|
for (const toolCall of toolCalls) {
|
|
287
271
|
const toolText = formatCliToolCall(toolCall);
|
|
288
272
|
if (toolText)
|
|
289
|
-
|
|
273
|
+
coalescer?.add('tool_call', toolText);
|
|
290
274
|
}
|
|
291
275
|
}
|
|
292
276
|
}
|
|
@@ -296,7 +280,12 @@ export function runKimi(config) {
|
|
|
296
280
|
}
|
|
297
281
|
}
|
|
298
282
|
});
|
|
299
|
-
proc.stderr?.on('data', (chunk) => {
|
|
283
|
+
proc.stderr?.on('data', (chunk) => {
|
|
284
|
+
// Kimi CLI 0.20.1 does not stream thinking to stderr incrementally during a
|
|
285
|
+
// -p run; stderr is empty until process exit. We therefore do not emit live
|
|
286
|
+
// `thought` progress events here and keep the existing end-of-run capture.
|
|
287
|
+
stderr = appendCapped(stderr, chunk.toString('utf-8'), MAX_CAPTURE_CHARS);
|
|
288
|
+
});
|
|
300
289
|
proc.on('error', (err) => {
|
|
301
290
|
finish({ ok: false, text: '', error: err instanceof Error ? err.message : String(err) });
|
|
302
291
|
});
|
package/dist/progress.js
CHANGED
|
@@ -21,6 +21,77 @@ export function makeEvent(kind, text) {
|
|
|
21
21
|
export function formatProgressLine(event) {
|
|
22
22
|
return `[${clockFromIso(event.at)}] ${KIND_GLYPH[event.kind]} ${event.kind}: ${event.text}`;
|
|
23
23
|
}
|
|
24
|
+
export const DEFAULT_DWELL_MS = 1_200;
|
|
25
|
+
export const PREVIEW_MAX_CHARS = 90;
|
|
26
|
+
export const TAIL_MAX_CHARS = 120;
|
|
27
|
+
function sliceByCodePoints(text, maxChars) {
|
|
28
|
+
return [...text].slice(-maxChars).join('');
|
|
29
|
+
}
|
|
30
|
+
function collapseWhitespace(text) {
|
|
31
|
+
return text.replace(/\s+/g, ' ').trim();
|
|
32
|
+
}
|
|
33
|
+
function formatPreview(tail, maxChars) {
|
|
34
|
+
const preview = collapseWhitespace(tail);
|
|
35
|
+
if (preview.length === 0)
|
|
36
|
+
return '';
|
|
37
|
+
const codePoints = [...preview];
|
|
38
|
+
if (codePoints.length <= maxChars)
|
|
39
|
+
return preview;
|
|
40
|
+
return `…${codePoints.slice(-maxChars).join('').trimStart()}`;
|
|
41
|
+
}
|
|
42
|
+
// Coalesces high-frequency `message`/`thought` streaming text into a single
|
|
43
|
+
// watchable preview line, emitted at most once per `dwellMs`. Action events
|
|
44
|
+
// (`tool_call`, `tool_update`, `plan`) are forwarded immediately, after flushing
|
|
45
|
+
// any pending preview so ordering is preserved.
|
|
46
|
+
export function createProgressCoalescer(reporter, options) {
|
|
47
|
+
const dwellMs = options?.dwellMs ?? DEFAULT_DWELL_MS;
|
|
48
|
+
const previewMaxChars = options?.previewMaxChars ?? PREVIEW_MAX_CHARS;
|
|
49
|
+
const tailMaxChars = options?.tailMaxChars ?? TAIL_MAX_CHARS;
|
|
50
|
+
const tails = { message: '', thought: '' };
|
|
51
|
+
let timer;
|
|
52
|
+
const emitTails = () => {
|
|
53
|
+
for (const kind of ['message', 'thought']) {
|
|
54
|
+
const tail = tails[kind];
|
|
55
|
+
if (!tail)
|
|
56
|
+
continue;
|
|
57
|
+
const text = formatPreview(tail, previewMaxChars);
|
|
58
|
+
tails[kind] = '';
|
|
59
|
+
if (text)
|
|
60
|
+
reporter(makeEvent(kind, text));
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
const flush = () => {
|
|
64
|
+
if (timer) {
|
|
65
|
+
clearTimeout(timer);
|
|
66
|
+
timer = undefined;
|
|
67
|
+
}
|
|
68
|
+
emitTails();
|
|
69
|
+
};
|
|
70
|
+
const schedule = () => {
|
|
71
|
+
if (timer)
|
|
72
|
+
return;
|
|
73
|
+
timer = setTimeout(() => {
|
|
74
|
+
timer = undefined;
|
|
75
|
+
emitTails();
|
|
76
|
+
}, dwellMs);
|
|
77
|
+
timer.unref?.();
|
|
78
|
+
};
|
|
79
|
+
const appendTail = (kind, text) => {
|
|
80
|
+
tails[kind] = sliceByCodePoints(tails[kind] + text, tailMaxChars);
|
|
81
|
+
};
|
|
82
|
+
const add = (kind, text) => {
|
|
83
|
+
if (kind === 'tool_call' || kind === 'tool_update' || kind === 'plan') {
|
|
84
|
+
flush();
|
|
85
|
+
reporter(makeEvent(kind, text));
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (kind === 'message' || kind === 'thought') {
|
|
89
|
+
appendTail(kind, text);
|
|
90
|
+
schedule();
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
return { add, flush, stop: flush };
|
|
94
|
+
}
|
|
24
95
|
export function createStallWatchdog(reporter, stallMs = DEFAULT_STALL_MS) {
|
|
25
96
|
let timer;
|
|
26
97
|
let stopped = false;
|
package/dist/transports/acp.js
CHANGED
|
@@ -4,7 +4,7 @@ import fs from 'node:fs/promises';
|
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { buildKimiEnv, resolveKimiPaths } from '../environment.js';
|
|
6
6
|
import { VERSION } from '../version.js';
|
|
7
|
-
import {
|
|
7
|
+
import { createProgressCoalescer, createStallWatchdog } from '../progress.js';
|
|
8
8
|
const MAX_ACP_FRAME_BYTES = 8 * 1024 * 1024;
|
|
9
9
|
const MAX_ACP_HEADER_BYTES = 16 * 1024;
|
|
10
10
|
const MAX_ACP_METADATA_BYTES = 100 * 1024;
|
|
@@ -551,43 +551,6 @@ function extractSessionId(value, fallback) {
|
|
|
551
551
|
}
|
|
552
552
|
return fallback;
|
|
553
553
|
}
|
|
554
|
-
const COALESCE_MS = 750;
|
|
555
|
-
const COALESCE_CHARS = 400;
|
|
556
|
-
// Streams of message/thought tokens are far too chatty to forward one event per
|
|
557
|
-
// chunk. Accumulate character counts and emit a single summarized event at most
|
|
558
|
-
// every COALESCE_MS or whenever the accumulated text exceeds COALESCE_CHARS.
|
|
559
|
-
// tool_call / tool_call_update / plan events are forwarded immediately.
|
|
560
|
-
function createCoalescingReporter(reporter) {
|
|
561
|
-
const counts = { message: 0, thought: 0 };
|
|
562
|
-
let timer;
|
|
563
|
-
const flush = () => {
|
|
564
|
-
if (timer) {
|
|
565
|
-
clearTimeout(timer);
|
|
566
|
-
timer = undefined;
|
|
567
|
-
}
|
|
568
|
-
for (const kind of ['message', 'thought']) {
|
|
569
|
-
if (counts[kind] > 0) {
|
|
570
|
-
reporter(makeEvent(kind, `${kind === 'message' ? 'streaming output' : 'thinking'}… (${counts[kind]} chars)`));
|
|
571
|
-
counts[kind] = 0;
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
};
|
|
575
|
-
const add = (kind, text) => {
|
|
576
|
-
if (kind !== 'message' && kind !== 'thought') {
|
|
577
|
-
flush();
|
|
578
|
-
reporter(makeEvent(kind, text));
|
|
579
|
-
return;
|
|
580
|
-
}
|
|
581
|
-
counts[kind] += text.length;
|
|
582
|
-
if (counts[kind] >= COALESCE_CHARS) {
|
|
583
|
-
flush();
|
|
584
|
-
}
|
|
585
|
-
else if (!timer) {
|
|
586
|
-
timer = setTimeout(flush, COALESCE_MS);
|
|
587
|
-
}
|
|
588
|
-
};
|
|
589
|
-
return { add, flush, stop: flush };
|
|
590
|
-
}
|
|
591
554
|
export async function runAcpPrompt(options) {
|
|
592
555
|
if (options.signal?.aborted)
|
|
593
556
|
return { ok: false, text: '', error: 'ACP prompt was aborted before start.' };
|
|
@@ -605,7 +568,7 @@ export async function runAcpPrompt(options) {
|
|
|
605
568
|
let watchdog;
|
|
606
569
|
let onNotification;
|
|
607
570
|
if (options.onProgress) {
|
|
608
|
-
coalescer =
|
|
571
|
+
coalescer = createProgressCoalescer(options.onProgress);
|
|
609
572
|
watchdog = createStallWatchdog(options.onProgress);
|
|
610
573
|
onNotification = (message) => {
|
|
611
574
|
watchdog?.ping();
|
|
@@ -676,7 +639,6 @@ export async function runAcpPrompt(options) {
|
|
|
676
639
|
finally {
|
|
677
640
|
if (onNotification)
|
|
678
641
|
client.off('notification', onNotification);
|
|
679
|
-
coalescer?.flush();
|
|
680
642
|
coalescer?.stop();
|
|
681
643
|
watchdog?.stop();
|
|
682
644
|
options.signal?.removeEventListener('abort', abort);
|