ladder-mcp 1.0.0 → 1.0.1

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,32 @@ 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.0.1] - 2026-06-28
8
+
9
+ Bug-fix release. Issues found by exercising all 20 tools live against a real
10
+ Kimi CLI (the mock-only unit tests had missed them).
11
+
12
+ ### Fixed
13
+
14
+ - **ACP responses lost spaces** (`kimi_chat`): streamed token chunks were each
15
+ trimmed and newline-joined, so `"Two plus two equals four."` came back as
16
+ `"Twoplustwoequalsfour."`. Text fragments are now concatenated as-is and only
17
+ the final string is trimmed. (CLI tools `kimi_analyze`/`kimi_resume` were never
18
+ affected.)
19
+ - **`kimi_export_session` silently no-op'd** while reporting `ok: true`: when no
20
+ session id was given, `kimi export` defaults to the most recent session and asks
21
+ `Export previous session …? [Y/n]` — a prompt that `-y` does not suppress in Kimi
22
+ CLI 0.20.1, so with stdin closed it exited 0 without writing. The tool now
23
+ resolves the most recent session id itself and passes it explicitly (skipping the
24
+ prompt), always passes `-y`, and verifies the output file exists before reporting
25
+ success.
26
+
27
+ ### Changed
28
+
29
+ - `kimi_acp_sessions` now accepts `limit` (default 20) and `work_dir` filters,
30
+ for parity with `kimi_list_sessions`; previously it dumped every ACP session
31
+ across all projects in one response.
32
+
7
33
  ## [1.0.0] - 2026-06-27
8
34
 
9
35
  First release. Windows-first MCP bridge for Kimi Code CLI v24, rebuilt in a
package/README.md CHANGED
@@ -6,7 +6,8 @@ client like Claude Code can run codebase analysis, native sessions, API
6
6
  queries, ACP chat, background tasks, and CLI admin/diagnostics — all on Windows
7
7
  without hardcoded POSIX assumptions.
8
8
 
9
- > Status: **v1.0.0**. Supported platform is **Windows 11 only**.
9
+ > Status: **v1.0.1** ([npm](https://www.npmjs.com/package/ladder-mcp)).
10
+ > Supported platform is **Windows 11 only**.
10
11
 
11
12
  ## Requirements
12
13
 
@@ -15,40 +16,58 @@ without hardcoded POSIX assumptions.
15
16
  - Kimi Code CLI installed (`kimi.exe` on PATH or at `~/.kimi-code/bin/kimi.exe`),
16
17
  authenticated (`~/.kimi-code/`)
17
18
 
18
- ## Install & build
19
+ ## Quick start (from npm)
19
20
 
20
- ```bash
21
- npm install
22
- npm run build # compiles src/ -> dist/ (tests excluded)
23
- ```
21
+ You don't need to clone or build — the package is published on npm and your MCP
22
+ client launches it via `npx`.
24
23
 
25
- Quick checks:
24
+ **Claude Code (one command):**
26
25
 
27
26
  ```bash
28
- npm test # vitest (44 tests)
29
- npm run typecheck # tsc --noEmit (incl. tests)
30
- npm run dev # run the server from source via tsx
27
+ claude mcp add kimi-code -- npx -y ladder-mcp
31
28
  ```
32
29
 
33
- ## Run as an MCP server
34
-
35
- The server speaks MCP over stdio. Point your MCP client at the built entry:
30
+ **Or add it manually to your MCP config:**
36
31
 
37
32
  ```jsonc
38
- // e.g. Claude Code MCP config
39
33
  {
40
34
  "mcpServers": {
41
35
  "kimi-code": {
42
- "command": "node",
43
- "args": ["C:\\path\\to\\Ladder_mcp\\dist\\index.js"]
36
+ "command": "npx",
37
+ "args": ["-y", "ladder-mcp"]
44
38
  }
45
39
  }
46
40
  }
47
41
  ```
48
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
+
49
53
  To let Kimi Code itself host this server, use the `kimi_generate_mcp_config`
50
54
  tool to produce/merge a `.kimi-code/mcp.json` entry.
51
55
 
56
+ ## Build from source (contributors)
57
+
58
+ ```bash
59
+ npm install
60
+ npm run build # compiles src/ -> dist/ (tests excluded)
61
+ ```
62
+
63
+ Quick checks:
64
+
65
+ ```bash
66
+ npm test # vitest (50 tests)
67
+ npm run typecheck # tsc --noEmit (incl. tests)
68
+ npm run dev # run the server from source via tsx
69
+ ```
70
+
52
71
  ## Tools
53
72
 
54
73
  **Core (v1)** — `kimi_analyze`, `kimi_query`, `kimi_verify`, `kimi_resume`,
package/dist/index.js CHANGED
@@ -219,8 +219,11 @@ server.tool('kimi_chat', 'Send a prompt through Kimi ACP over stdio. Set backgro
219
219
  });
220
220
  return textResponse(JSON.stringify(result, null, 2), !result.ok);
221
221
  });
222
- server.tool('kimi_acp_sessions', 'List Kimi ACP sessions through `session/list`.', {}, async () => {
223
- const result = await listAcpSessions();
222
+ server.tool('kimi_acp_sessions', 'List Kimi ACP sessions through `session/list`.', {
223
+ limit: z.number().int().positive().optional(),
224
+ work_dir: z.string().optional(),
225
+ }, async ({ limit, work_dir }) => {
226
+ const result = await listAcpSessions({ limit, workDir: work_dir });
224
227
  return textResponse(result.ok ? result.text : `Error: ${result.error}`, !result.ok);
225
228
  });
226
229
  server.tool('kimi_cancel', 'Cancel an active Ladder task by task_id or a Kimi ACP session by session_id.', {
@@ -153,7 +153,11 @@ function extractTextDeep(value) {
153
153
  return [...direct, ...nested];
154
154
  }
155
155
  export function extractAcpText(value) {
156
- return extractTextDeep(value).map((part) => part.trim()).filter(Boolean).join('\n');
156
+ // Concatenate text fragments as-is. Kimi streams the answer as many small
157
+ // agent_message_chunk tokens (e.g. "Two", " plus", " two"); trimming each
158
+ // fragment or joining on newlines would drop the spaces between tokens and
159
+ // run words together. Callers trim the final assembled string.
160
+ return extractTextDeep(value).join('');
157
161
  }
158
162
  // Keep the trailing portion of `text` within a UTF-8 *byte* budget. Slicing by
159
163
  // String length (UTF-16 code units) under-counts multibyte characters, so the
@@ -357,7 +361,9 @@ export async function runAcpPrompt(options) {
357
361
  sessionId = extractSessionId(await client.newSession(options.workDir));
358
362
  }
359
363
  const result = await client.prompt(sessionId, options.prompt, options.workDir);
360
- let text = extractAcpText(result) || client.getUpdateText();
364
+ // extractAcpText no longer trims fragments (to preserve inter-token spaces),
365
+ // so trim the fully assembled string here. getUpdateText already trims.
366
+ let text = extractAcpText(result).trim() || client.getUpdateText();
361
367
  if (!text) {
362
368
  try {
363
369
  text = JSON.stringify(result, null, 2);
@@ -385,11 +391,28 @@ export async function runAcpPrompt(options) {
385
391
  client.close();
386
392
  }
387
393
  }
388
- export async function listAcpSessions() {
394
+ const DEFAULT_ACP_SESSION_LIMIT = 20;
395
+ function normalizePath(value) {
396
+ return value.replace(/\\/g, '/').replace(/\/+$/, '').toLowerCase();
397
+ }
398
+ export async function listAcpSessions(options = {}) {
389
399
  const client = new AcpClient(60_000);
390
400
  try {
391
401
  await client.initialize();
392
402
  const result = await client.listSessions();
403
+ // Filter by working directory and cap the count for parity with kimi_list_sessions;
404
+ // session/list otherwise returns every ACP session across all projects in one blob.
405
+ const sessions = result?.sessions;
406
+ if (Array.isArray(sessions)) {
407
+ let filtered = sessions;
408
+ if (options.workDir) {
409
+ const target = normalizePath(options.workDir);
410
+ filtered = filtered.filter((s) => typeof s?.cwd === 'string' && normalizePath(s.cwd) === target);
411
+ }
412
+ const limit = options.limit ?? DEFAULT_ACP_SESSION_LIMIT;
413
+ const limited = limit > 0 ? filtered.slice(0, limit) : filtered;
414
+ return { ok: true, text: JSON.stringify({ sessions: limited, total: filtered.length }, null, 2) };
415
+ }
393
416
  return { ok: true, text: JSON.stringify(result, null, 2) };
394
417
  }
395
418
  catch (error) {
@@ -3,6 +3,7 @@ import * as fs from 'node:fs';
3
3
  import * as path from 'node:path';
4
4
  import { promisify } from 'node:util';
5
5
  import { buildKimiEnv, getKimiStatus, resolveKimiPaths } from '../environment.js';
6
+ import { listSessions } from '../session-store.js';
6
7
  const execFileAsync = promisify(execFile);
7
8
  const ADMIN_TIMEOUT_MS = 30_000;
8
9
  // Quote a single argument for the human-readable preview command. This string is
@@ -76,8 +77,12 @@ export function buildExportArgs(options) {
76
77
  if (options.sessionId)
77
78
  args.push(options.sessionId);
78
79
  args.push('-o', options.outputPath);
79
- if (options.overwriteExisting === true)
80
- args.push('-y');
80
+ // Always auto-confirm. `kimi export` prompts "Export previous session …? [Y/n]"
81
+ // even for a brand-new path; since we close stdin, that prompt would get EOF and
82
+ // the export would silently no-op while still exiting 0. Overwrite safety is
83
+ // enforced by our own pre-check (assertSafeOutputPath + statSync + overwrite_existing),
84
+ // so unconditionally passing -y is safe.
85
+ args.push('-y');
81
86
  if (options.includeGlobalLog !== true)
82
87
  args.push('--no-include-global-log');
83
88
  return args;
@@ -179,6 +184,18 @@ export async function exportKimiSession(options) {
179
184
  error: error instanceof Error ? error.message : String(error),
180
185
  };
181
186
  }
187
+ // Resolve an explicit session id ourselves when the caller omitted one. `kimi export`
188
+ // otherwise defaults to "the most recent session" and asks "Export previous session …?
189
+ // [Y/n]" — a prompt that `-y` does NOT suppress in Kimi CLI 0.20.1, so with stdin closed
190
+ // it hangs/aborts and writes nothing. Passing an explicit id skips that prompt entirely.
191
+ let sessionId = options.sessionId;
192
+ if (!sessionId) {
193
+ const recent = listSessions({ limit: 1 })[0];
194
+ if (!recent) {
195
+ return { ok: false, stdout: '', stderr: '', error: 'No Kimi session found to export. Provide an explicit session_id.' };
196
+ }
197
+ sessionId = recent.id;
198
+ }
182
199
  const outputPath = path.resolve(options.outputPath);
183
200
  let existingStat;
184
201
  try {
@@ -207,7 +224,18 @@ export async function exportKimiSession(options) {
207
224
  };
208
225
  }
209
226
  }
210
- return runKimiCommand(buildExportArgs({ ...options, outputPath }), 120_000);
227
+ const result = await runKimiCommand(buildExportArgs({ ...options, sessionId, outputPath }), 120_000);
228
+ // The CLI can exit 0 without producing the archive (e.g. a declined/aborted prompt).
229
+ // Verify the file actually exists so we never report success for a no-op export.
230
+ if (result.ok && !fs.existsSync(outputPath)) {
231
+ return {
232
+ ok: false,
233
+ stdout: result.stdout,
234
+ stderr: result.stderr,
235
+ error: 'Kimi export exited without creating the output file. No archive was written.',
236
+ };
237
+ }
238
+ return result;
211
239
  }
212
240
  export function visualizeSession(options) {
213
241
  const binary = requireBinary();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ladder-mcp",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Windows-first MCP bridge for Kimi Code CLI v24",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -19,7 +19,8 @@
19
19
  "build": "tsc -p tsconfig.build.json",
20
20
  "start": "node dist/index.js",
21
21
  "test": "vitest run",
22
- "typecheck": "tsc --noEmit"
22
+ "typecheck": "tsc --noEmit",
23
+ "prepublishOnly": "npm run build && npm test"
23
24
  },
24
25
  "engines": {
25
26
  "node": ">=18.0.0"