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 +38 -0
- package/README.md +110 -110
- package/dist/index.js +2 -2
- package/dist/kimi-runner.js +9 -4
- package/dist/progress.js +176 -5
- package/dist/transports/acp.js +7 -1
- package/package.json +1 -1
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.
|
|
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 (
|
|
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,
|
|
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(
|
|
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) => {
|
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 { createProgressCoalescer,
|
|
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
|
|
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
|
-
|
|
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;
|
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 { 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;
|