ladder-mcp 1.1.2 → 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 +80 -0
- package/README.md +110 -110
- package/dist/index.js +3 -3
- package/dist/kimi-runner.js +34 -40
- package/dist/progress.js +244 -2
- package/dist/transports/acp.js +8 -40
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,86 @@ 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
|
+
|
|
45
|
+
## [1.1.4] - 2026-06-28
|
|
46
|
+
|
|
47
|
+
Make the live-progress line worth watching.
|
|
48
|
+
|
|
49
|
+
### Changed
|
|
50
|
+
|
|
51
|
+
- Live progress now shows a readable content **preview** of the streamed
|
|
52
|
+
`message`/`thought` text (whitespace collapsed, code-point-safe truncation with
|
|
53
|
+
an ellipsis) instead of a bare `(N chars)` counter.
|
|
54
|
+
- Progress emission is throttled to at most one update per ~1.2s, so the single
|
|
55
|
+
progress line stops flickering and each line stays readable. `tool_call`,
|
|
56
|
+
`tool_update`, and `plan` events remain immediate and flush any pending preview
|
|
57
|
+
first to preserve order.
|
|
58
|
+
- The two per-transport coalescers (CLI and ACP) are unified into one shared
|
|
59
|
+
`createProgressCoalescer` in `src/progress.ts`; the duplicated `COALESCE_*`
|
|
60
|
+
constants are gone.
|
|
61
|
+
|
|
62
|
+
### Notes
|
|
63
|
+
|
|
64
|
+
- Kimi CLI 0.20.1 does not stream thinking to stderr during `-p` runs, so live
|
|
65
|
+
`thought` progress is not available on the CLI transport (documented in code).
|
|
66
|
+
The ACP transport already emits `thought`, which now gets the preview treatment.
|
|
67
|
+
|
|
68
|
+
## [1.1.3] - 2026-06-28
|
|
69
|
+
|
|
70
|
+
Bugfix: analysis-mode `kimi_code` was broken against Kimi CLI 0.20.1.
|
|
71
|
+
|
|
72
|
+
### Fixed
|
|
73
|
+
|
|
74
|
+
- `kimi_code` without `edit: true` (the default analysis mode) crashed against
|
|
75
|
+
Kimi CLI 0.20.1 with `Cannot combine --prompt with --plan`. `buildKimiArgs` no
|
|
76
|
+
longer passes `--plan` in `-p` prompt mode (the CLI rejects it, along with
|
|
77
|
+
`--auto`/`-y`).
|
|
78
|
+
|
|
79
|
+
### Changed
|
|
80
|
+
|
|
81
|
+
- Because Kimi 0.20.1 has no `-p`-compatible read-only flag, the `edit: false`
|
|
82
|
+
analysis-only contract is now enforced at the prompt level: `runKimi` prepends
|
|
83
|
+
a read-only guard (`applyReadOnlyGuard`) when `edit` is not `true`. This is
|
|
84
|
+
advisory (the model is instructed not to edit) rather than a hard CLI
|
|
85
|
+
guarantee; the `edit` parameter description documents the change.
|
|
86
|
+
|
|
7
87
|
## [1.1.2] - 2026-06-28
|
|
8
88
|
|
|
9
89
|
Live-progress and cancellation reliability for `kimi_code`.
|
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',
|
|
@@ -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."),
|
|
@@ -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 {
|
|
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
|
|
@@ -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,17 +210,15 @@ export function runKimi(config) {
|
|
|
225
210
|
let stderr = '';
|
|
226
211
|
let timedOut = false;
|
|
227
212
|
let streamBuffer = '';
|
|
228
|
-
const coalescer = config.onProgress ?
|
|
229
|
-
const
|
|
213
|
+
const coalescer = config.onProgress ? createProgressCoalescer(config.onProgress) : undefined;
|
|
214
|
+
const todoTracker = config.onProgress ? createTodoTracker() : undefined;
|
|
230
215
|
let onAbort;
|
|
231
216
|
const finish = (result) => {
|
|
232
217
|
if (settled)
|
|
233
218
|
return;
|
|
234
219
|
settled = true;
|
|
235
220
|
clearTimeout(timer);
|
|
236
|
-
coalescer?.flush();
|
|
237
221
|
coalescer?.stop();
|
|
238
|
-
watchdog?.stop();
|
|
239
222
|
config.signal?.removeEventListener('abort', onAbort);
|
|
240
223
|
resolve(result);
|
|
241
224
|
};
|
|
@@ -262,7 +245,6 @@ export function runKimi(config) {
|
|
|
262
245
|
stdout = appendCapped(stdout, chunk.toString('utf-8'), MAX_CAPTURE_CHARS);
|
|
263
246
|
if (!config.onProgress)
|
|
264
247
|
return;
|
|
265
|
-
watchdog?.ping();
|
|
266
248
|
streamBuffer += chunk.toString('utf-8');
|
|
267
249
|
const lines = streamBuffer.split(/\r?\n/);
|
|
268
250
|
streamBuffer = lines.pop() ?? '';
|
|
@@ -275,28 +257,40 @@ export function runKimi(config) {
|
|
|
275
257
|
if (record.type === 'plan') {
|
|
276
258
|
const text = contentToText(record.content).trim();
|
|
277
259
|
if (text)
|
|
278
|
-
|
|
260
|
+
coalescer?.add('plan', text);
|
|
279
261
|
}
|
|
280
262
|
else if (record.role === 'assistant') {
|
|
281
263
|
const text = contentToText(record.content);
|
|
282
264
|
if (text.trim())
|
|
283
|
-
coalescer?.add(text);
|
|
265
|
+
coalescer?.add('message', text);
|
|
284
266
|
const toolCalls = record.tool_calls;
|
|
285
267
|
if (Array.isArray(toolCalls)) {
|
|
286
268
|
for (const toolCall of toolCalls) {
|
|
287
269
|
const toolText = formatCliToolCall(toolCall);
|
|
288
270
|
if (toolText)
|
|
289
|
-
|
|
271
|
+
coalescer?.add('tool_call', toolText);
|
|
290
272
|
}
|
|
291
273
|
}
|
|
292
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
|
+
}
|
|
293
282
|
}
|
|
294
283
|
catch {
|
|
295
284
|
// Ignore non-JSON status lines.
|
|
296
285
|
}
|
|
297
286
|
}
|
|
298
287
|
});
|
|
299
|
-
proc.stderr?.on('data', (chunk) => {
|
|
288
|
+
proc.stderr?.on('data', (chunk) => {
|
|
289
|
+
// Kimi CLI 0.20.1 does not stream thinking to stderr incrementally during a
|
|
290
|
+
// -p run; stderr is empty until process exit. We therefore do not emit live
|
|
291
|
+
// `thought` progress events here and keep the existing end-of-run capture.
|
|
292
|
+
stderr = appendCapped(stderr, chunk.toString('utf-8'), MAX_CAPTURE_CHARS);
|
|
293
|
+
});
|
|
300
294
|
proc.on('error', (err) => {
|
|
301
295
|
finish({ ok: false, text: '', error: err instanceof Error ? err.message : String(err) });
|
|
302
296
|
});
|
package/dist/progress.js
CHANGED
|
@@ -7,20 +7,262 @@ 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
|
+
}
|
|
149
|
+
export const DEFAULT_DWELL_MS = 1_200;
|
|
150
|
+
export const DEFAULT_TODO_PRIORITY_MS = 5_000;
|
|
151
|
+
export const PREVIEW_MAX_CHARS = 90;
|
|
152
|
+
export const TAIL_MAX_CHARS = 120;
|
|
153
|
+
function sliceByCodePoints(text, maxChars) {
|
|
154
|
+
return [...text].slice(-maxChars).join('');
|
|
155
|
+
}
|
|
156
|
+
function collapseWhitespace(text) {
|
|
157
|
+
return text.replace(/\s+/g, ' ').trim();
|
|
158
|
+
}
|
|
159
|
+
function formatPreview(tail, maxChars) {
|
|
160
|
+
const preview = collapseWhitespace(tail);
|
|
161
|
+
if (preview.length === 0)
|
|
162
|
+
return '';
|
|
163
|
+
const codePoints = [...preview];
|
|
164
|
+
if (codePoints.length <= maxChars)
|
|
165
|
+
return preview;
|
|
166
|
+
return `…${codePoints.slice(-maxChars).join('').trimStart()}`;
|
|
167
|
+
}
|
|
168
|
+
// Coalesces high-frequency `message`/`thought` streaming text into a single
|
|
169
|
+
// watchable preview line, emitted at most once per `dwellMs`. Action events
|
|
170
|
+
// (`tool_call`, `tool_update`, `plan`) are forwarded immediately, after flushing
|
|
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.
|
|
174
|
+
export function createProgressCoalescer(reporter, options) {
|
|
175
|
+
const dwellMs = options?.dwellMs ?? DEFAULT_DWELL_MS;
|
|
176
|
+
const previewMaxChars = options?.previewMaxChars ?? PREVIEW_MAX_CHARS;
|
|
177
|
+
const tailMaxChars = options?.tailMaxChars ?? TAIL_MAX_CHARS;
|
|
178
|
+
const todoPriorityMs = options?.todoPriorityMs ?? DEFAULT_TODO_PRIORITY_MS;
|
|
179
|
+
const tails = { message: '', thought: '' };
|
|
180
|
+
let timer;
|
|
181
|
+
let priorityTimer;
|
|
182
|
+
let priorityUntil = 0;
|
|
183
|
+
const emitTails = () => {
|
|
184
|
+
for (const kind of ['message', 'thought']) {
|
|
185
|
+
const tail = tails[kind];
|
|
186
|
+
if (!tail)
|
|
187
|
+
continue;
|
|
188
|
+
const text = formatPreview(tail, previewMaxChars);
|
|
189
|
+
tails[kind] = '';
|
|
190
|
+
if (text)
|
|
191
|
+
reporter(makeEvent(kind, text));
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
const flush = () => {
|
|
195
|
+
if (timer) {
|
|
196
|
+
clearTimeout(timer);
|
|
197
|
+
timer = undefined;
|
|
198
|
+
}
|
|
199
|
+
emitTails();
|
|
200
|
+
};
|
|
201
|
+
const clearPriority = () => {
|
|
202
|
+
priorityUntil = 0;
|
|
203
|
+
if (priorityTimer) {
|
|
204
|
+
clearTimeout(priorityTimer);
|
|
205
|
+
priorityTimer = undefined;
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
const schedule = () => {
|
|
209
|
+
if (timer)
|
|
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
|
+
}
|
|
226
|
+
timer = setTimeout(() => {
|
|
227
|
+
timer = undefined;
|
|
228
|
+
emitTails();
|
|
229
|
+
}, dwellMs);
|
|
230
|
+
timer.unref?.();
|
|
231
|
+
};
|
|
232
|
+
const appendTail = (kind, text) => {
|
|
233
|
+
tails[kind] = sliceByCodePoints(tails[kind] + text, tailMaxChars);
|
|
234
|
+
};
|
|
235
|
+
const add = (kind, text, metadata) => {
|
|
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
|
+
}
|
|
243
|
+
flush();
|
|
244
|
+
reporter(makeEvent(kind, text));
|
|
245
|
+
return;
|
|
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
|
+
}
|
|
255
|
+
if (kind === 'message' || kind === 'thought') {
|
|
256
|
+
appendTail(kind, text);
|
|
257
|
+
schedule();
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
const stop = () => {
|
|
261
|
+
clearPriority();
|
|
262
|
+
flush();
|
|
263
|
+
};
|
|
264
|
+
return { add, flush, stop };
|
|
265
|
+
}
|
|
24
266
|
export function createStallWatchdog(reporter, stallMs = DEFAULT_STALL_MS) {
|
|
25
267
|
let timer;
|
|
26
268
|
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 { createStallWatchdog,
|
|
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;
|
|
@@ -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.' };
|
|
@@ -603,10 +566,12 @@ export async function runAcpPrompt(options) {
|
|
|
603
566
|
}
|
|
604
567
|
let coalescer;
|
|
605
568
|
let watchdog;
|
|
569
|
+
let todoTracker;
|
|
606
570
|
let onNotification;
|
|
607
571
|
if (options.onProgress) {
|
|
608
|
-
coalescer =
|
|
572
|
+
coalescer = createProgressCoalescer(options.onProgress);
|
|
609
573
|
watchdog = createStallWatchdog(options.onProgress);
|
|
574
|
+
todoTracker = createTodoTracker();
|
|
610
575
|
onNotification = (message) => {
|
|
611
576
|
watchdog?.ping();
|
|
612
577
|
const params = message.params;
|
|
@@ -623,6 +588,10 @@ export async function runAcpPrompt(options) {
|
|
|
623
588
|
case 'tool_call_update':
|
|
624
589
|
case 'plan': {
|
|
625
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
|
+
}
|
|
626
595
|
const kind = updateType === 'tool_call' ? 'tool_call' : updateType === 'tool_call_update' ? 'tool_update' : 'plan';
|
|
627
596
|
coalescer?.add(kind, text);
|
|
628
597
|
break;
|
|
@@ -676,7 +645,6 @@ export async function runAcpPrompt(options) {
|
|
|
676
645
|
finally {
|
|
677
646
|
if (onNotification)
|
|
678
647
|
client.off('notification', onNotification);
|
|
679
|
-
coalescer?.flush();
|
|
680
648
|
coalescer?.stop();
|
|
681
649
|
watchdog?.stop();
|
|
682
650
|
options.signal?.removeEventListener('abort', abort);
|