ladder-mcp 1.1.4 → 1.1.5

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,44 @@ 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.5] - 2026-06-28
8
+
9
+ Turn Kimi's TODO/plan into a watchable live checklist.
10
+
11
+ ### Added
12
+
13
+ - Kimi's working TODO list is now surfaced as a `todo` progress event in both
14
+ transports. A shared `createTodoTracker` holds the current `{ text, status }`
15
+ list (replaced on each update) and renders a compact one-line summary
16
+ (`TODO 2/4 ✓✓·· · now: <current item>`) for the live progress line, plus the
17
+ full multi-line snapshot in the background task log.
18
+ - **CLI**: `role:tool` `TodoList` results (the `Current todo list:` text
19
+ snapshot) are parsed and tracked. **ACP**: structured `tool_call_update`/`plan`
20
+ TODO payloads — JSON `{"todos":[{title,status}]}` or the text snapshot — are
21
+ parsed into the same tracker. The normal `tool_call`/`plan` action event is
22
+ still emitted alongside the `todo` event, so actions stay visible.
23
+ - The progress coalescer gains a TODO-priority dwell window (`todoPriorityMs`,
24
+ default 5s): after a TODO update, chatty `message`/`thought` previews are
25
+ suppressed so the checklist line stays readable, while `tool_call`,
26
+ `tool_update`, and `plan` actions still surface immediately. Suppressed preview
27
+ tails are never lost — they flush on the next TODO, on window expiry, and on
28
+ stop/cancel.
29
+
30
+ ### Changed
31
+
32
+ - The background task log now uses `formatTaskLogLine`, which indents the full
33
+ TODO snapshot under the compact summary; foreground/MCP progress stays on the
34
+ compact `formatProgressLine`.
35
+ - Removed the CLI stall watchdog. Kimi CLI 0.20.1 works silently during long
36
+ thinking phases, so the watchdog produced false `stall` warnings. The ACP
37
+ transport keeps its watchdog (it streams granular per-action activity).
38
+
39
+ ### Notes
40
+
41
+ - Additive change: the existing `tool_call`/`tool_update`/`message`/`thought`/
42
+ `plan` events and the task log are otherwise unchanged — `todo` is an added
43
+ layer. No new runtime dependency; `kimi-code-mcp/` untouched.
44
+
7
45
  ## [1.1.4] - 2026-06-28
8
46
 
9
47
  Make the live-progress line worth watching.
package/README.md CHANGED
@@ -1,110 +1,110 @@
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.1.4** ([npm](https://www.npmjs.com/package/ladder-mcp)).
10
- > Supported platform is **Windows 11 only**.
11
-
12
- ## Requirements
13
-
14
- - Windows 11
15
- - Node.js ≥ 18
16
- - Kimi Code CLI installed (`kimi.exe` on PATH or at `~/.kimi-code/bin/kimi.exe`),
17
- authenticated (`~/.kimi-code/`)
18
-
19
- ## Quick start (from npm)
20
-
21
- You don't need to clone or build — the package is published on npm and your MCP
22
- client launches it via `npx`, or you can install the package directly.
23
-
24
- **Claude Code (one command):**
25
-
26
- ```bash
27
- claude mcp add kimi-code -- npx -y ladder-mcp
28
- ```
29
-
30
- **Or add it manually to your MCP config:**
31
-
32
- ```jsonc
33
- {
34
- "mcpServers": {
35
- "kimi-code": {
36
- "command": "npx",
37
- "args": ["-y", "ladder-mcp"]
38
- }
39
- }
40
- }
41
- ```
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
-
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`
63
- tool to produce/merge a `.kimi-code/mcp.json` entry.
64
-
65
- ## Build from source (contributors)
66
-
67
- ```bash
68
- npm install
69
- npm run build # compiles src/ -> dist/ (tests excluded)
70
- ```
71
-
72
- Quick checks:
73
-
74
- ```bash
75
- npm test # vitest (163 tests)
76
- npm run typecheck # tsc --noEmit (incl. tests)
77
- npm run dev # run the server from source via tsx
78
- ```
79
-
80
- ## Tools
81
-
82
- **Core (v1.1)** — `kimi_code`, `kimi_ask`, `kimi_sessions`, `kimi_tasks`,
83
- `kimi_status`, `kimi_setup`
84
-
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.
88
-
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`.
92
-
93
- ## Safety boundaries
94
-
95
- - `kimi_export_session` requires an explicit `output_path`, stays within the
96
- working directory, and excludes the global diagnostic log by default.
97
- - Desktop Work tools are **experimental and read-only**: they do not read the
98
- desktop token store, replay web auth, or submit desktop Work tasks.
99
- - The vendored `kimi-code-mcp/` is a **read-only reference** and is never edited
100
- or written to by the tools.
101
-
102
- ## Project layout
103
-
104
- - `src/` — the Ladder_mcp application (package `ladder-mcp`)
105
- - `kimi-code-mcp/` — upstream reference (read-only, MIT)
106
- - `_bmad-output/` — planning & implementation artifacts (BMAD workflow)
107
-
108
- ## License
109
-
110
- [MIT](./LICENSE). Ports logic from the MIT-licensed `kimi-code-mcp` reference.
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.1.5** ([npm](https://www.npmjs.com/package/ladder-mcp)).
10
+ > Supported platform is **Windows 11 only**.
11
+
12
+ ## Requirements
13
+
14
+ - Windows 11
15
+ - Node.js ≥ 18
16
+ - Kimi Code CLI installed (`kimi.exe` on PATH or at `~/.kimi-code/bin/kimi.exe`),
17
+ authenticated (`~/.kimi-code/`)
18
+
19
+ ## Quick start (from npm)
20
+
21
+ You don't need to clone or build — the package is published on npm and your MCP
22
+ client launches it via `npx`, or you can install the package directly.
23
+
24
+ **Claude Code (one command):**
25
+
26
+ ```bash
27
+ claude mcp add kimi-code -- npx -y ladder-mcp
28
+ ```
29
+
30
+ **Or add it manually to your MCP config:**
31
+
32
+ ```jsonc
33
+ {
34
+ "mcpServers": {
35
+ "kimi-code": {
36
+ "command": "npx",
37
+ "args": ["-y", "ladder-mcp"]
38
+ }
39
+ }
40
+ }
41
+ ```
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
+
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`
63
+ tool to produce/merge a `.kimi-code/mcp.json` entry.
64
+
65
+ ## Build from source (contributors)
66
+
67
+ ```bash
68
+ npm install
69
+ npm run build # compiles src/ -> dist/ (tests excluded)
70
+ ```
71
+
72
+ Quick checks:
73
+
74
+ ```bash
75
+ npm test # vitest (174 tests)
76
+ npm run typecheck # tsc --noEmit (incl. tests)
77
+ npm run dev # run the server from source via tsx
78
+ ```
79
+
80
+ ## Tools
81
+
82
+ **Core (v1.1)** — `kimi_code`, `kimi_ask`, `kimi_sessions`, `kimi_tasks`,
83
+ `kimi_status`, `kimi_setup`
84
+
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.
88
+
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`.
92
+
93
+ ## Safety boundaries
94
+
95
+ - `kimi_export_session` requires an explicit `output_path`, stays within the
96
+ working directory, and excludes the global diagnostic log by default.
97
+ - Desktop Work tools are **experimental and read-only**: they do not read the
98
+ desktop token store, replay web auth, or submit desktop Work tasks.
99
+ - The vendored `kimi-code-mcp/` is a **read-only reference** and is never edited
100
+ or written to by the tools.
101
+
102
+ ## Project layout
103
+
104
+ - `src/` — the Ladder_mcp application (package `ladder-mcp`)
105
+ - `kimi-code-mcp/` — upstream reference (read-only, MIT)
106
+ - `_bmad-output/` — planning & implementation artifacts (BMAD workflow)
107
+
108
+ ## License
109
+
110
+ [MIT](./LICENSE). Ports logic from the MIT-licensed `kimi-code-mcp` reference.
package/dist/index.js CHANGED
@@ -14,7 +14,7 @@ import { exportKimiSession, getKimiCapabilities, listKimiProviders, runKimiDocto
14
14
  import { generateMcpConfig } from './kimi-mcp-config.js';
15
15
  import { buildBudgetProbeGuide, getDesktopStatus } from './desktop-work.js';
16
16
  import { taskStore } from './task-store.js';
17
- import { combineReporters, createMcpProgressReporter, formatProgressLine } from './progress.js';
17
+ import { combineReporters, createMcpProgressReporter, formatTaskLogLine } from './progress.js';
18
18
  import { VERSION } from './version.js';
19
19
  const server = new McpServer({
20
20
  name: 'kimi-code',
@@ -75,7 +75,7 @@ server.tool('kimi_code', 'Agentic work in a repository: analyze and edit files.
75
75
  return textResponse(`Error: ${workDirError}`, true);
76
76
  const includeThinkingValue = include_thinking ?? false;
77
77
  const effectiveTransport = transport ?? 'cli';
78
- const appendReporter = (append) => (event) => append(formatProgressLine(event));
78
+ const appendReporter = (append) => (event) => append(formatTaskLogLine(event));
79
79
  if (effectiveTransport === 'cli') {
80
80
  if (background) {
81
81
  const task = taskStore.create('cli-code', async (_signal, append) => {
@@ -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 { createProgressCoalescer, createStallWatchdog } from './progress.js';
5
+ import { createProgressCoalescer, createTodoTracker, parseCliTodoSnapshot } 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
@@ -211,7 +211,7 @@ export function runKimi(config) {
211
211
  let timedOut = false;
212
212
  let streamBuffer = '';
213
213
  const coalescer = config.onProgress ? createProgressCoalescer(config.onProgress) : undefined;
214
- const watchdog = config.onProgress ? createStallWatchdog(config.onProgress) : undefined;
214
+ const todoTracker = config.onProgress ? createTodoTracker() : undefined;
215
215
  let onAbort;
216
216
  const finish = (result) => {
217
217
  if (settled)
@@ -219,7 +219,6 @@ export function runKimi(config) {
219
219
  settled = true;
220
220
  clearTimeout(timer);
221
221
  coalescer?.stop();
222
- watchdog?.stop();
223
222
  config.signal?.removeEventListener('abort', onAbort);
224
223
  resolve(result);
225
224
  };
@@ -246,7 +245,6 @@ export function runKimi(config) {
246
245
  stdout = appendCapped(stdout, chunk.toString('utf-8'), MAX_CAPTURE_CHARS);
247
246
  if (!config.onProgress)
248
247
  return;
249
- watchdog?.ping();
250
248
  streamBuffer += chunk.toString('utf-8');
251
249
  const lines = streamBuffer.split(/\r?\n/);
252
250
  streamBuffer = lines.pop() ?? '';
@@ -274,6 +272,13 @@ export function runKimi(config) {
274
272
  }
275
273
  }
276
274
  }
275
+ else if (record.role === 'tool') {
276
+ const text = contentToText(record.content);
277
+ const todos = parseCliTodoSnapshot(text);
278
+ if (todos && todoTracker?.update(todos)) {
279
+ coalescer?.add('todo', todoTracker.getSummary(), { todoFullList: todoTracker.getFullList() });
280
+ }
281
+ }
277
282
  }
278
283
  catch {
279
284
  // Ignore non-JSON status lines.
package/dist/progress.js CHANGED
@@ -7,21 +7,147 @@ const KIND_GLYPH = {
7
7
  tool_update: '⚙',
8
8
  plan: '◇',
9
9
  stall: '⚠',
10
+ todo: '☑',
10
11
  };
11
12
  function clockFromIso(iso) {
12
13
  // HH:MM:SS in local time; falls back to the raw value if it is not a valid date.
13
14
  const date = new Date(iso);
14
15
  return Number.isNaN(date.getTime()) ? iso : date.toTimeString().slice(0, 8);
15
16
  }
16
- export function makeEvent(kind, text) {
17
- return { kind, text, at: new Date().toISOString() };
17
+ export function makeEvent(kind, text, metadata) {
18
+ return { kind, text, at: new Date().toISOString(), metadata };
18
19
  }
19
20
  // One compact line per event for the background task log, e.g.
20
21
  // `[12:01:07] ⚙ tool: edit src/index.ts`.
21
22
  export function formatProgressLine(event) {
22
23
  return `[${clockFromIso(event.at)}] ${KIND_GLYPH[event.kind]} ${event.kind}: ${event.text}`;
23
24
  }
25
+ // Task-log variant: for TODO events, append the full multi-line list so the
26
+ // accumulating background log contains complete snapshots, while live progress
27
+ // surfaces only the compact one-line summary.
28
+ export function formatTaskLogLine(event) {
29
+ const base = formatProgressLine(event);
30
+ const fullList = event.metadata?.todoFullList;
31
+ if (event.kind === 'todo' && fullList) {
32
+ return `${base}\n${fullList.split('\n').map((line) => ` ${line}`).join('\n')}`;
33
+ }
34
+ return base;
35
+ }
36
+ const TODO_STATUS_MARKERS = {
37
+ done: '✓',
38
+ in_progress: '·',
39
+ pending: '·',
40
+ };
41
+ export function formatTodoSummary(items) {
42
+ if (items.length === 0)
43
+ return 'TODO 0/0';
44
+ const done = items.filter((i) => i.status === 'done').length;
45
+ const markers = items.map((i) => TODO_STATUS_MARKERS[i.status]).join('');
46
+ const current = items.find((i) => i.status === 'in_progress');
47
+ const now = current ? ` · now: ${current.text}` : '';
48
+ return `TODO ${done}/${items.length} ${markers}${now}`;
49
+ }
50
+ export function formatTodoFullList(items) {
51
+ if (items.length === 0)
52
+ return '(empty todo list)';
53
+ return items.map((i) => `[${i.status}] ${i.text}`).join('\n');
54
+ }
55
+ export function createTodoTracker() {
56
+ let items = [];
57
+ const sameItems = (a, b) => {
58
+ if (a.length !== b.length)
59
+ return false;
60
+ return a.every((item, idx) => item.text === b[idx].text && item.status === b[idx].status);
61
+ };
62
+ return {
63
+ update(next) {
64
+ if (sameItems(items, next))
65
+ return false;
66
+ items = next.map((i) => ({ text: i.text.trim(), status: i.status }));
67
+ return true;
68
+ },
69
+ getItems() {
70
+ return items;
71
+ },
72
+ getSummary() {
73
+ return formatTodoSummary(items);
74
+ },
75
+ getFullList() {
76
+ return formatTodoFullList(items);
77
+ },
78
+ clear() {
79
+ items = [];
80
+ },
81
+ };
82
+ }
83
+ const TODO_SNAPSHOT_MARKER = 'Current todo list:';
84
+ const TODO_BOILERPLATE_MARKER = 'Ensure that you continue to use the todo list';
85
+ // Parses the human-readable snapshot that Kimi's TodoList tool returns in CLI
86
+ // `role:tool` results and in ACP `tool_call_update` content.
87
+ export function parseCliTodoSnapshot(text) {
88
+ const markerIndex = text.indexOf(TODO_SNAPSHOT_MARKER);
89
+ if (markerIndex < 0)
90
+ return undefined;
91
+ const start = markerIndex + TODO_SNAPSHOT_MARKER.length;
92
+ const raw = text.slice(start);
93
+ // Stop before the trailing boilerplate if present.
94
+ const end = raw.indexOf(TODO_BOILERPLATE_MARKER);
95
+ const section = (end < 0 ? raw : raw.slice(0, end)).trim();
96
+ if (!section)
97
+ return undefined;
98
+ const items = [];
99
+ for (const line of section.split(/\r?\n/)) {
100
+ const trimmed = line.trim();
101
+ const match = trimmed.match(/^\[(done|in_progress|pending)\]\s*(.+)$/) ?? trimmed.match(/^(done|in_progress|pending):\s*(.+)$/i);
102
+ if (!match) {
103
+ // First non-matching blank line is tolerated; a second non-matching line
104
+ // marks the end of the list.
105
+ if (items.length > 0)
106
+ break;
107
+ continue;
108
+ }
109
+ const status = match[1].toLowerCase();
110
+ const text = match[2].trim();
111
+ if (text)
112
+ items.push({ status, text });
113
+ }
114
+ return items.length > 0 ? items : undefined;
115
+ }
116
+ // Parses either a JSON `{"todos":[{"title":"...","status":"..."}]}` payload
117
+ // (common in ACP `tool_call_update`) or a human-readable snapshot.
118
+ export function parseAcpTodoPayload(text) {
119
+ const trimmed = text.trim();
120
+ if (trimmed.startsWith('{')) {
121
+ try {
122
+ const parsed = JSON.parse(trimmed);
123
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
124
+ const record = parsed;
125
+ const todos = record.todos ?? record.items ?? record.plan;
126
+ if (Array.isArray(todos)) {
127
+ const items = [];
128
+ for (const entry of todos) {
129
+ if (!entry || typeof entry !== 'object')
130
+ continue;
131
+ const e = entry;
132
+ const text = typeof e.title === 'string' ? e.title : typeof e.content === 'string' ? e.content : typeof e.text === 'string' ? e.text : '';
133
+ const rawStatus = typeof e.status === 'string' ? e.status.toLowerCase() : 'pending';
134
+ const status = rawStatus === 'done' || rawStatus === 'completed' || rawStatus === 'complete' ? 'done' : rawStatus === 'in_progress' || rawStatus === 'in progress' || rawStatus === 'active' ? 'in_progress' : 'pending';
135
+ if (text.trim())
136
+ items.push({ text: text.trim(), status });
137
+ }
138
+ if (items.length > 0)
139
+ return items;
140
+ }
141
+ }
142
+ }
143
+ catch {
144
+ // Fall through to snapshot parsing.
145
+ }
146
+ }
147
+ return parseCliTodoSnapshot(text);
148
+ }
24
149
  export const DEFAULT_DWELL_MS = 1_200;
150
+ export const DEFAULT_TODO_PRIORITY_MS = 5_000;
25
151
  export const PREVIEW_MAX_CHARS = 90;
26
152
  export const TAIL_MAX_CHARS = 120;
27
153
  function sliceByCodePoints(text, maxChars) {
@@ -42,13 +168,18 @@ function formatPreview(tail, maxChars) {
42
168
  // Coalesces high-frequency `message`/`thought` streaming text into a single
43
169
  // watchable preview line, emitted at most once per `dwellMs`. Action events
44
170
  // (`tool_call`, `tool_update`, `plan`) are forwarded immediately, after flushing
45
- // any pending preview so ordering is preserved.
171
+ // any pending preview so ordering is preserved. `todo` events are also immediate
172
+ // and start a short priority window during which `message`/`thought` previews are
173
+ // suppressed so the checklist line stays readable.
46
174
  export function createProgressCoalescer(reporter, options) {
47
175
  const dwellMs = options?.dwellMs ?? DEFAULT_DWELL_MS;
48
176
  const previewMaxChars = options?.previewMaxChars ?? PREVIEW_MAX_CHARS;
49
177
  const tailMaxChars = options?.tailMaxChars ?? TAIL_MAX_CHARS;
178
+ const todoPriorityMs = options?.todoPriorityMs ?? DEFAULT_TODO_PRIORITY_MS;
50
179
  const tails = { message: '', thought: '' };
51
180
  let timer;
181
+ let priorityTimer;
182
+ let priorityUntil = 0;
52
183
  const emitTails = () => {
53
184
  for (const kind of ['message', 'thought']) {
54
185
  const tail = tails[kind];
@@ -67,9 +198,31 @@ export function createProgressCoalescer(reporter, options) {
67
198
  }
68
199
  emitTails();
69
200
  };
201
+ const clearPriority = () => {
202
+ priorityUntil = 0;
203
+ if (priorityTimer) {
204
+ clearTimeout(priorityTimer);
205
+ priorityTimer = undefined;
206
+ }
207
+ };
70
208
  const schedule = () => {
71
209
  if (timer)
72
210
  return;
211
+ const now = Date.now();
212
+ if (now < priorityUntil) {
213
+ // Message/thought previews are deferred while a TODO line is on screen.
214
+ // Arm a single timer that will emit the accumulated tail once the window
215
+ // expires; additional streaming text during the window only updates tails.
216
+ if (!priorityTimer) {
217
+ priorityTimer = setTimeout(() => {
218
+ priorityTimer = undefined;
219
+ priorityUntil = 0;
220
+ emitTails();
221
+ }, priorityUntil - now);
222
+ priorityTimer.unref?.();
223
+ }
224
+ return;
225
+ }
73
226
  timer = setTimeout(() => {
74
227
  timer = undefined;
75
228
  emitTails();
@@ -79,18 +232,36 @@ export function createProgressCoalescer(reporter, options) {
79
232
  const appendTail = (kind, text) => {
80
233
  tails[kind] = sliceByCodePoints(tails[kind] + text, tailMaxChars);
81
234
  };
82
- const add = (kind, text) => {
235
+ const add = (kind, text, metadata) => {
83
236
  if (kind === 'tool_call' || kind === 'tool_update' || kind === 'plan') {
237
+ // During a TODO priority window, action events still surface but chatty
238
+ // message/thought previews stay suppressed so the checklist remains visible.
239
+ if (Date.now() < priorityUntil) {
240
+ reporter(makeEvent(kind, text));
241
+ return;
242
+ }
84
243
  flush();
85
244
  reporter(makeEvent(kind, text));
86
245
  return;
87
246
  }
247
+ if (kind === 'todo') {
248
+ flush();
249
+ clearPriority();
250
+ priorityUntil = Date.now() + todoPriorityMs;
251
+ reporter(makeEvent(kind, text, metadata));
252
+ schedule();
253
+ return;
254
+ }
88
255
  if (kind === 'message' || kind === 'thought') {
89
256
  appendTail(kind, text);
90
257
  schedule();
91
258
  }
92
259
  };
93
- return { add, flush, stop: flush };
260
+ const stop = () => {
261
+ clearPriority();
262
+ flush();
263
+ };
264
+ return { add, flush, stop };
94
265
  }
95
266
  export function createStallWatchdog(reporter, stallMs = DEFAULT_STALL_MS) {
96
267
  let timer;
@@ -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 { createProgressCoalescer, createStallWatchdog } from '../progress.js';
7
+ import { createProgressCoalescer, createStallWatchdog, createTodoTracker, parseAcpTodoPayload } 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;
@@ -566,10 +566,12 @@ export async function runAcpPrompt(options) {
566
566
  }
567
567
  let coalescer;
568
568
  let watchdog;
569
+ let todoTracker;
569
570
  let onNotification;
570
571
  if (options.onProgress) {
571
572
  coalescer = createProgressCoalescer(options.onProgress);
572
573
  watchdog = createStallWatchdog(options.onProgress);
574
+ todoTracker = createTodoTracker();
573
575
  onNotification = (message) => {
574
576
  watchdog?.ping();
575
577
  const params = message.params;
@@ -586,6 +588,10 @@ export async function runAcpPrompt(options) {
586
588
  case 'tool_call_update':
587
589
  case 'plan': {
588
590
  const text = extractAcpText(update?.content) || JSON.stringify(update);
591
+ const todos = parseAcpTodoPayload(text);
592
+ if (todos && todoTracker?.update(todos)) {
593
+ coalescer?.add('todo', todoTracker.getSummary(), { todoFullList: todoTracker.getFullList() });
594
+ }
589
595
  const kind = updateType === 'tool_call' ? 'tool_call' : updateType === 'tool_call_update' ? 'tool_update' : 'plan';
590
596
  coalescer?.add(kind, text);
591
597
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ladder-mcp",
3
- "version": "1.1.4",
3
+ "version": "1.1.5",
4
4
  "description": "Windows-first MCP bridge for Kimi Code CLI v24",
5
5
  "license": "MIT",
6
6
  "type": "module",