kimi-plugin-cc 2.5.4 → 2.5.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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +8 -0
- package/dist/commands/setup.js +57 -6
- package/dist/hooks/managed-block.js +70 -71
- package/dist/kimi-version-probe.js +25 -7
- package/dist/version.js +1 -1
- package/docs/registry-releases.md +4 -2
- package/docs/upstream-2.5.0-certification.md +2 -0
- package/docs/upstream-2.5.1-certification.md +2 -0
- package/package.json +1 -1
- package/plugins/kimi-codex/.codex-plugin/plugin.json +1 -1
- package/plugins/kimi-codex/dist/commands/setup.js +57 -6
- package/plugins/kimi-codex/dist/hooks/managed-block.js +70 -71
- package/plugins/kimi-codex/dist/kimi-version-probe.js +25 -7
- package/plugins/kimi-codex/dist/version.js +1 -1
- package/plugins/kimi-dsh/dist/commands/setup.js +57 -6
- package/plugins/kimi-dsh/dist/hooks/managed-block.js +70 -71
- package/plugins/kimi-dsh/dist/kimi-version-probe.js +25 -7
- package/plugins/kimi-dsh/dist/version.js +1 -1
- package/plugins/kimi-dsh/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kimi",
|
|
3
3
|
"description": "Kimi for Claude Code: code questions, independent reviews, bounded fixes, autonomous goals, parallel tasks, and MCP tasks through the local Kimi Code CLI, with PreToolUse safety hooks.",
|
|
4
|
-
"version": "2.5.
|
|
4
|
+
"version": "2.5.5",
|
|
5
5
|
"homepage": "https://github.com/linxule/kimi-plugin-cc",
|
|
6
6
|
"repository": "https://github.com/linxule/kimi-plugin-cc",
|
|
7
7
|
"license": "Apache-2.0",
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
> **Post-1.0 release history (v1.0.1 -> present) lives in [ROADMAP-TO-GA.md § Post-GA audit log](./ROADMAP-TO-GA.md#post-ga-audit-log)** and the "Version" / "Upstream compat" lines of [AGENTS.md](./AGENTS.md). Docs-only kimi-code compat checkups that don't bump the plugin version (e.g. the 0.14.2 / 0.14.3 patches) are recorded there, not here. Notable releases are summarized below; the GA entry and full pre-GA detail follow.
|
|
4
4
|
|
|
5
|
+
## 2.5.5 — 2026-11-07
|
|
6
|
+
|
|
7
|
+
- Keep every host's managed hook block in one contiguous run: setup inserts a new block directly after the last managed END when only blanks/comments follow it and the parser proves the result equals the appended form, otherwise appends. A block previously appended after Magpie's model tables lost its BEGIN marker when Magpie rewrote them (yetone/magpie#5614), leaving an `END-without-BEGIN` orphan.
|
|
8
|
+
- Scope orphan markers per host. Markers pair only as an adjacent same-host BEGIN→END; another host's orphan is reported by `setup --check` and no longer fails this host, while own and legacy orphans still fail closed. The orphan remedy is plain `/kimi:setup`, never a scoped `--uninstall`, which would disarm a healthy block and leave the other host's orphan.
|
|
9
|
+
- Parse the whole config for the marker-less (absent-state) fallback instead of deleting managed-block lines first. Deleting them could hide a `matcher` that TOML binds to this host's bare hook table from a key written before another host's `[[hooks]]` header, reporting a disabled hook as installed; this predates 2.5.5 and was found in pre-tag review. Host isolation now counts clean exact-command entries against managed blocks carrying that command.
|
|
10
|
+
- Name the native-v2 certified versions in the out-of-range version notice and measure "newer" against the newest exact one, instead of the legacy-v1 0.41.x bound.
|
|
11
|
+
- Pass the full repository gate with 1,421 tests and 18,320 assertions, 330-file package checks across all three distributions, isolated consumer security, 24 real-binary smoke tests (167 assertions, no skips) on exact 2.5.1, and independent Codex and Kimi pre-tag review. Exact CLI certification remains unchanged through 2.5.1; publication does not update installed hosts or operator pins.
|
|
12
|
+
|
|
5
13
|
## 2.5.4 — 2026-11-06
|
|
6
14
|
|
|
7
15
|
- Restore the original platform-neutral bounded-close settlement for swarm agent-state verification after follow-up CI showed that `wasClean` can both accept an early close and reject a valid one. The published 2.5.3 artifact contains the trial condition; this patch carries its revert without re-tagging or republishing it.
|
package/dist/commands/setup.js
CHANGED
|
@@ -26,9 +26,9 @@ import { readPluginConfig, writePluginConfig } from "../config.js";
|
|
|
26
26
|
import { RuntimeError } from "../errors.js";
|
|
27
27
|
import { normalizeTopLevelInlineHooks, validateKimiHookSetForEnvironment, withKimiConfigLock, } from "../hooks/config-safety.js";
|
|
28
28
|
import { buildHookShellCommand, hostIdFromHookCommand, resolveHookScriptPath, resolveHostId, resolveNodeBinary, } from "../hooks/install-paths.js";
|
|
29
|
-
import { decodeManagedCommandLine, effectiveHost, evaluateInstalled, findUnmanagedApprovalHookBlocks, markerCommentLines, MARKERS, parseManagedBlock, } from "../hooks/managed-block.js";
|
|
29
|
+
import { decodeManagedCommandLine, effectiveHost, evaluateInstalled, findUnmanagedApprovalHookBlocks, markerCommentLines, MARKERS, ORPHAN_MARKER_REMEDY, parseManagedBlock, } from "../hooks/managed-block.js";
|
|
30
30
|
import { formatVersionOutOfRangeWarning, probeKimiVersion, } from "../kimi-version-probe.js";
|
|
31
|
-
import { isNativeV2CertifiedVersion } from "../kimi-engine.js";
|
|
31
|
+
import { isNativeV2CertifiedVersion, NATIVE_V2_CERTIFIED_VERSIONS } from "../kimi-engine.js";
|
|
32
32
|
import { resolveKimiHome } from "../kimi-home.js";
|
|
33
33
|
import { ensurePluginPaths, resolvePluginPaths } from "../paths.js";
|
|
34
34
|
import { KIMI_PLUGIN_CC_VERSION } from "../version.js";
|
|
@@ -180,7 +180,7 @@ async function runInstall(configPath, hookScriptPath, hostId, reviewGateEnabled,
|
|
|
180
180
|
const block = buildManagedBlock(hookScriptPath, context.env, lineEnding, hostId);
|
|
181
181
|
const next = state.kind === "found"
|
|
182
182
|
? spliceBlock(existing, state.beginLine, state.endLine, block, lineEnding)
|
|
183
|
-
:
|
|
183
|
+
: insertBlockNearManagedBlocks(existing, blocks, block, lineEnding);
|
|
184
184
|
await assertKimiHookSetValid(next, configPath, "setup.install", context.env);
|
|
185
185
|
const blockWritten = next !== original;
|
|
186
186
|
if (blockWritten)
|
|
@@ -256,7 +256,10 @@ async function runCheck(configPath, hookScriptPath, hostId, reviewGateEnabled, w
|
|
|
256
256
|
nodeExists: (binPath) => existsSync(binPath),
|
|
257
257
|
});
|
|
258
258
|
// Non-blocking coexistence + cruft notices.
|
|
259
|
-
const { blocks: allBlocks } = parseManagedBlock(existing, hostId);
|
|
259
|
+
const { blocks: allBlocks, foreignOrphans } = parseManagedBlock(existing, hostId);
|
|
260
|
+
for (const orphan of foreignOrphans) {
|
|
261
|
+
warnings.push(`Host "${orphan.hostId}" has an orphaned ${orphan.detail} managed marker at line ${orphan.line + 1}; that host's commands refuse until it is repaired. ${ORPHAN_MARKER_REMEDY}`);
|
|
262
|
+
}
|
|
260
263
|
const otherHosts = [
|
|
261
264
|
...new Set(allBlocks.map((b) => effectiveHost(b, hostId)).filter((h) => h !== hostId)),
|
|
262
265
|
];
|
|
@@ -303,7 +306,9 @@ async function runCheck(configPath, hookScriptPath, hostId, reviewGateEnabled, w
|
|
|
303
306
|
reviewGateEnabled,
|
|
304
307
|
nextStep: installedCheck.state.kind === "absent"
|
|
305
308
|
? "Run /kimi:setup (without --check) to install the managed block."
|
|
306
|
-
:
|
|
309
|
+
: installedCheck.state.kind === "orphan"
|
|
310
|
+
? ORPHAN_MARKER_REMEDY
|
|
311
|
+
: "Run /kimi:setup --uninstall, then /kimi:setup to repair.",
|
|
307
312
|
details: buildDetails({
|
|
308
313
|
configPath,
|
|
309
314
|
hookScriptPath,
|
|
@@ -535,6 +540,52 @@ function spliceBlock(contents, beginLine, endLine, replacement, lineEnding) {
|
|
|
535
540
|
const replacementWithEnding = replacement.endsWith(lineEnding) ? replacement : `${replacement}${lineEnding}`;
|
|
536
541
|
return `${beforeWithSep}${replacementWithEnding}${after}`;
|
|
537
542
|
}
|
|
543
|
+
// Keep every host's block in one contiguous run. Magpie's table rewrite drops
|
|
544
|
+
// comments that follow a table it owns (yetone/magpie#5614): a block appended
|
|
545
|
+
// after such a table loses its BEGIN marker and header, leaving an orphan END.
|
|
546
|
+
// Insert after the last managed END only when nothing but blanks/comments
|
|
547
|
+
// precedes the next header AND the real parser proves the result equals the
|
|
548
|
+
// appended form apart from hook order, so no field can rebind and no value can
|
|
549
|
+
// be split; otherwise append.
|
|
550
|
+
function insertBlockNearManagedBlocks(contents, blocks, block, lineEnding) {
|
|
551
|
+
if (blocks.length === 0)
|
|
552
|
+
return appendBlock(contents, block, lineEnding);
|
|
553
|
+
const anchor = Math.max(...blocks.map((b) => b.endLine));
|
|
554
|
+
const lines = splitPreservingEnding(contents);
|
|
555
|
+
for (let i = anchor + 1; i < lines.length; i += 1) {
|
|
556
|
+
const trimmed = lines[i].trim();
|
|
557
|
+
if (trimmed.startsWith("["))
|
|
558
|
+
break;
|
|
559
|
+
if (trimmed.length > 0 && !trimmed.startsWith("#"))
|
|
560
|
+
return appendBlock(contents, block, lineEnding);
|
|
561
|
+
}
|
|
562
|
+
const rawLines = contents.split(/(?<=\n)/);
|
|
563
|
+
const head = rawLines.slice(0, anchor + 1).join("");
|
|
564
|
+
const tail = rawLines.slice(anchor + 1).join("");
|
|
565
|
+
const appended = appendBlock(contents, block, lineEnding);
|
|
566
|
+
if (tail.length === 0)
|
|
567
|
+
return appended;
|
|
568
|
+
const headWithEnding = head.endsWith("\n") ? head : `${head}${lineEnding}`;
|
|
569
|
+
const body = block.endsWith(lineEnding) ? block : `${block}${lineEnding}`;
|
|
570
|
+
const tailSeparator = /^\r?\n/.test(tail) ? "" : lineEnding;
|
|
571
|
+
const inserted = `${headWithEnding}${lineEnding}${body}${tailSeparator}${tail}`;
|
|
572
|
+
return sameTomlIgnoringHookOrder(inserted, appended) ? inserted : appended;
|
|
573
|
+
}
|
|
574
|
+
function sameTomlIgnoringHookOrder(a, b) {
|
|
575
|
+
const canonical = (text) => {
|
|
576
|
+
const parsed = parseToml(text);
|
|
577
|
+
const hooks = Array.isArray(parsed.hooks)
|
|
578
|
+
? parsed.hooks.map((entry) => JSON.stringify(entry)).sort()
|
|
579
|
+
: parsed.hooks;
|
|
580
|
+
return JSON.stringify({ ...parsed, hooks });
|
|
581
|
+
};
|
|
582
|
+
try {
|
|
583
|
+
return canonical(a) === canonical(b);
|
|
584
|
+
}
|
|
585
|
+
catch {
|
|
586
|
+
return false;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
538
589
|
function appendBlock(contents, block, lineEnding) {
|
|
539
590
|
const base = contents.length === 0 || contents.endsWith(lineEnding) ? contents : `${contents}${lineEnding}`;
|
|
540
591
|
const separator = base.length === 0 ? "" : lineEnding;
|
|
@@ -928,7 +979,7 @@ async function collectKimiVersionWarnings(env, warnings) {
|
|
|
928
979
|
// v2-certified binary would wrongly warn "not certified, commands will refuse".
|
|
929
980
|
if (probe.inTestedRange || isNativeV2CertifiedVersion(probe.version))
|
|
930
981
|
return;
|
|
931
|
-
warnings.push(formatVersionOutOfRangeWarning(probe, KIMI_PLUGIN_CC_VERSION));
|
|
982
|
+
warnings.push(formatVersionOutOfRangeWarning(probe, KIMI_PLUGIN_CC_VERSION, NATIVE_V2_CERTIFIED_VERSIONS));
|
|
932
983
|
}
|
|
933
984
|
/**
|
|
934
985
|
* Non-blocking notice for kimi-code's own enabled plugins from
|
|
@@ -26,6 +26,12 @@
|
|
|
26
26
|
import { parse as parseToml } from "../vendor/smol-toml/parse.js";
|
|
27
27
|
import { classifyHookCommandDrift, describeHookCommandDrift, hostIdFromHookCommand, isOurApprovalHookCommand, } from "./install-paths.js";
|
|
28
28
|
const MARKER_TAG = "kimi-plugin-cc-managed";
|
|
29
|
+
/**
|
|
30
|
+
* Plain install removes unpaired marker comments for every host and leaves each
|
|
31
|
+
* host's hook table in place. A scoped uninstall removes only this host's
|
|
32
|
+
* markers, so it would disarm a healthy block and keep another host's orphan.
|
|
33
|
+
*/
|
|
34
|
+
export const ORPHAN_MARKER_REMEDY = "Run /kimi:setup (without --uninstall or --check) to repair: install removes orphaned managed marker comments for every host and keeps each host's hook table. A scoped --uninstall does not remove another host's orphan marker. Another tool rewriting ~/.kimi-code/config.toml usually causes this.";
|
|
29
35
|
/**
|
|
30
36
|
* Strict BEGIN matcher. Accepts `# === BEGIN kimi-plugin-cc-managed`
|
|
31
37
|
* with an optional `:<host-id>` suffix, an optional ` (vX.Y.Z)` suffix,
|
|
@@ -135,40 +141,41 @@ function findMarkers(lines) {
|
|
|
135
141
|
return hits;
|
|
136
142
|
}
|
|
137
143
|
/**
|
|
138
|
-
* Pair BEGIN/END markers into well-formed blocks.
|
|
139
|
-
* (
|
|
144
|
+
* Pair BEGIN/END markers into well-formed blocks. A block is a BEGIN followed
|
|
145
|
+
* immediately (in marker order) by an END with the same host suffix; every
|
|
146
|
+
* other marker is an orphan. Nested or interleaved markers therefore orphan
|
|
147
|
+
* both sides rather than being guessed into a block. Callers decide which
|
|
148
|
+
* orphans fail closed (see `parseManagedBlock`).
|
|
140
149
|
*/
|
|
141
150
|
function pairBlocks(lines) {
|
|
142
151
|
const markers = findMarkers(lines);
|
|
143
152
|
const blocks = [];
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
153
|
+
const orphans = [];
|
|
154
|
+
let i = 0;
|
|
155
|
+
while (i < markers.length) {
|
|
156
|
+
const marker = markers[i];
|
|
157
|
+
const next = markers[i + 1];
|
|
158
|
+
if (marker.kind === "begin" && next !== undefined && next.kind === "end" && next.hostId === marker.hostId) {
|
|
159
|
+
const body = validateBlockBody(lines.slice(marker.index + 1, next.index));
|
|
160
|
+
blocks.push({
|
|
161
|
+
hostId: marker.hostId,
|
|
162
|
+
beginLine: marker.index,
|
|
163
|
+
endLine: next.index,
|
|
164
|
+
commandPath: body.commandPath,
|
|
165
|
+
valid: body.valid,
|
|
166
|
+
invalidReason: body.invalidReason,
|
|
167
|
+
});
|
|
168
|
+
i += 2;
|
|
151
169
|
continue;
|
|
152
170
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
return { orphan: "BEGIN-without-END" };
|
|
158
|
-
const body = validateBlockBody(lines.slice(open.index + 1, marker.index));
|
|
159
|
-
blocks.push({
|
|
160
|
-
hostId: open.hostId,
|
|
161
|
-
beginLine: open.index,
|
|
162
|
-
endLine: marker.index,
|
|
163
|
-
commandPath: body.commandPath,
|
|
164
|
-
valid: body.valid,
|
|
165
|
-
invalidReason: body.invalidReason,
|
|
171
|
+
orphans.push({
|
|
172
|
+
line: marker.index,
|
|
173
|
+
detail: marker.kind === "begin" ? "BEGIN-without-END" : "END-without-BEGIN",
|
|
174
|
+
hostId: marker.hostId,
|
|
166
175
|
});
|
|
167
|
-
|
|
176
|
+
i += 1;
|
|
168
177
|
}
|
|
169
|
-
|
|
170
|
-
return { orphan: "BEGIN-without-END" };
|
|
171
|
-
return { blocks };
|
|
178
|
+
return { blocks, orphans };
|
|
172
179
|
}
|
|
173
180
|
/** Validate the lines BETWEEN a BEGIN/END pair against the safety grammar. */
|
|
174
181
|
function validateBlockBody(rawBody) {
|
|
@@ -274,24 +281,30 @@ function validateBlockBody(rawBody) {
|
|
|
274
281
|
export function parseManagedBlock(contents, hostId) {
|
|
275
282
|
const rawLines = contents.split("\n");
|
|
276
283
|
const lines = rawLines.map((line) => line.replace(/\r$/, ""));
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
|
|
284
|
+
const { blocks, orphans } = pairBlocks(lines);
|
|
285
|
+
// An orphan suffixed for ANOTHER host cannot be part of this host's block:
|
|
286
|
+
// this host's verdict still rests on its own well-formed pair, the whole-file
|
|
287
|
+
// hook-set validation, and the real-parser cleanliness check. Legacy and own
|
|
288
|
+
// orphans stay fail-closed because they may be this host's damaged block.
|
|
289
|
+
const ownOrphan = orphans.find((o) => o.hostId === null || o.hostId === hostId);
|
|
290
|
+
const foreignOrphans = orphans.filter((o) => o.hostId !== null && o.hostId !== hostId);
|
|
291
|
+
if (ownOrphan !== undefined) {
|
|
292
|
+
return { state: { kind: "orphan", detail: ownOrphan.detail }, blocks: [], lines, foreignOrphans };
|
|
280
293
|
}
|
|
281
|
-
const blocks = paired.blocks;
|
|
282
294
|
// A block belongs to this host when its marker suffix matches or, for a
|
|
283
295
|
// legacy un-suffixed block, its command derives to this host. An unattributable
|
|
284
296
|
// legacy command is claimable by the next installer; a command attributed to
|
|
285
297
|
// another host is ignored so its block remains untouched.
|
|
286
298
|
const relevant = blocks.filter((b) => effectiveHost(b, hostId) === hostId);
|
|
287
299
|
if (relevant.length === 0) {
|
|
288
|
-
return { state: { kind: "absent" }, blocks, lines };
|
|
300
|
+
return { state: { kind: "absent" }, blocks, lines, foreignOrphans };
|
|
289
301
|
}
|
|
290
302
|
if (relevant.length > 1) {
|
|
291
303
|
return {
|
|
292
304
|
state: { kind: "duplicate", beginLines: relevant.map((b) => b.beginLine) },
|
|
293
305
|
blocks,
|
|
294
306
|
lines,
|
|
307
|
+
foreignOrphans,
|
|
295
308
|
};
|
|
296
309
|
}
|
|
297
310
|
const block = relevant[0];
|
|
@@ -307,6 +320,7 @@ export function parseManagedBlock(contents, hostId) {
|
|
|
307
320
|
},
|
|
308
321
|
blocks,
|
|
309
322
|
lines,
|
|
323
|
+
foreignOrphans,
|
|
310
324
|
};
|
|
311
325
|
}
|
|
312
326
|
/**
|
|
@@ -335,27 +349,6 @@ export function decodeManagedCommandLine(trimmedLine) {
|
|
|
335
349
|
return literal[1];
|
|
336
350
|
return null;
|
|
337
351
|
}
|
|
338
|
-
/**
|
|
339
|
-
* Return `contents` with every well-formed managed BEGIN/END block removed
|
|
340
|
-
* (inclusive), so what remains is only the bare/root TOML. Used by
|
|
341
|
-
* `hasCleanEnforcingHookEntry` to enforce host isolation. If the markers are
|
|
342
|
-
* unbalanced (orphan), returns the input unchanged — the caller is only reached
|
|
343
|
-
* in the `absent` state, where a well-formed foreign block is the norm.
|
|
344
|
-
*/
|
|
345
|
-
function stripManagedBlockLines(contents) {
|
|
346
|
-
const lines = contents.split("\n").map((line) => line.replace(/\r$/, ""));
|
|
347
|
-
const paired = pairBlocks(lines);
|
|
348
|
-
if (!("blocks" in paired))
|
|
349
|
-
return contents;
|
|
350
|
-
const inBlock = new Set();
|
|
351
|
-
for (const block of paired.blocks) {
|
|
352
|
-
for (let i = block.beginLine; i <= block.endLine; i += 1)
|
|
353
|
-
inBlock.add(i);
|
|
354
|
-
}
|
|
355
|
-
if (inBlock.size === 0)
|
|
356
|
-
return contents;
|
|
357
|
-
return lines.filter((_, i) => !inBlock.has(i)).join("\n");
|
|
358
|
-
}
|
|
359
352
|
/**
|
|
360
353
|
* True if a parsed `hooks[]` array contains a clean enforcing entry for
|
|
361
354
|
* `expectedCommand`: `event="PreToolUse"`, byte-exact `command`, NO `matcher`
|
|
@@ -364,10 +357,11 @@ function stripManagedBlockLines(contents) {
|
|
|
364
357
|
* kimi-code's strict schema). Each host's hook path is distinct, so the command
|
|
365
358
|
* uniquely identifies this host's entry.
|
|
366
359
|
*/
|
|
367
|
-
function
|
|
360
|
+
function countCleanEntries(hooks, expectedCommand) {
|
|
368
361
|
if (!Array.isArray(hooks))
|
|
369
|
-
return
|
|
362
|
+
return 0;
|
|
370
363
|
const allowedKeys = new Set(["event", "command", "timeout"]);
|
|
364
|
+
let count = 0;
|
|
371
365
|
for (const entry of hooks) {
|
|
372
366
|
if (entry === null || typeof entry !== "object")
|
|
373
367
|
continue;
|
|
@@ -380,29 +374,37 @@ function hooksArrayHasCleanEntry(hooks, expectedCommand) {
|
|
|
380
374
|
continue;
|
|
381
375
|
if (!Object.keys(rec).every((k) => allowedKeys.has(k)))
|
|
382
376
|
continue;
|
|
383
|
-
|
|
377
|
+
count += 1;
|
|
384
378
|
}
|
|
385
|
-
return
|
|
379
|
+
return count;
|
|
380
|
+
}
|
|
381
|
+
function hooksArrayHasCleanEntry(hooks, expectedCommand) {
|
|
382
|
+
return countCleanEntries(hooks, expectedCommand) > 0;
|
|
386
383
|
}
|
|
387
384
|
/**
|
|
388
|
-
* Parser-based fallback used only in the absent state.
|
|
389
|
-
*
|
|
390
|
-
*
|
|
391
|
-
*
|
|
392
|
-
*
|
|
393
|
-
*
|
|
385
|
+
* Parser-based fallback used only in the absent state. Parse the WHOLE file
|
|
386
|
+
* exactly as kimi-code does: deleting managed-block lines first would change
|
|
387
|
+
* which table their keys bind to (a `matcher` written before another host's
|
|
388
|
+
* `[[hooks]]` header belongs to the preceding table, possibly this host's bare
|
|
389
|
+
* one). Host isolation is kept by counting: every managed block whose body
|
|
390
|
+
* carries this exact command may account for one clean entry, so only a clean
|
|
391
|
+
* entry beyond those can be this host's marker-less table. Requires the exact
|
|
392
|
+
* event and command, no matcher in any spelling, and no foreign keys. Parse
|
|
393
|
+
* errors fail closed. Callers run whole-file schema validation first.
|
|
394
394
|
*/
|
|
395
395
|
export function hasCleanEnforcingHookEntry(contents, expectedCommand) {
|
|
396
396
|
let parsed;
|
|
397
397
|
try {
|
|
398
|
-
parsed = parseToml(
|
|
398
|
+
parsed = parseToml(contents);
|
|
399
399
|
}
|
|
400
400
|
catch {
|
|
401
401
|
return false;
|
|
402
402
|
}
|
|
403
403
|
if (parsed === null || typeof parsed !== "object")
|
|
404
404
|
return false;
|
|
405
|
-
|
|
405
|
+
const lines = contents.split("\n").map((line) => line.replace(/\r$/, ""));
|
|
406
|
+
const managedWithCommand = pairBlocks(lines).blocks.filter((block) => block.commandPath === expectedCommand).length;
|
|
407
|
+
return countCleanEntries(parsed.hooks, expectedCommand) > managedWithCommand;
|
|
406
408
|
}
|
|
407
409
|
/**
|
|
408
410
|
* Whole-file parser check for the FOUND (marked-block present) path. The line
|
|
@@ -468,7 +470,7 @@ export function evaluateInstalled(contents, expectedCommand, opts) {
|
|
|
468
470
|
if (state.kind === "orphan") {
|
|
469
471
|
return {
|
|
470
472
|
installed: false,
|
|
471
|
-
reason: `${state.detail} marker.
|
|
473
|
+
reason: `${state.detail} marker. ${ORPHAN_MARKER_REMEDY}`,
|
|
472
474
|
state,
|
|
473
475
|
};
|
|
474
476
|
}
|
|
@@ -516,12 +518,9 @@ export function findBareApprovalHookTables(contents) {
|
|
|
516
518
|
const lines = contents.split("\n").map((line) => line.replace(/\r$/, ""));
|
|
517
519
|
const tableLines = scanTomlLines(contents).tables;
|
|
518
520
|
const managed = new Set();
|
|
519
|
-
const
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
for (let i = block.beginLine; i <= block.endLine; i += 1)
|
|
523
|
-
managed.add(i);
|
|
524
|
-
}
|
|
521
|
+
for (const block of pairBlocks(lines).blocks) {
|
|
522
|
+
for (let i = block.beginLine; i <= block.endLine; i += 1)
|
|
523
|
+
managed.add(i);
|
|
525
524
|
}
|
|
526
525
|
const ranges = [];
|
|
527
526
|
for (let i = 0; i < lines.length; i += 1) {
|
|
@@ -194,23 +194,41 @@ export function maxTestedMinor() {
|
|
|
194
194
|
? entry
|
|
195
195
|
: max);
|
|
196
196
|
}
|
|
197
|
+
function compareVersions(a, b) {
|
|
198
|
+
const pa = a.split(".").map((part) => Number.parseInt(part, 10));
|
|
199
|
+
const pb = b.split(".").map((part) => Number.parseInt(part, 10));
|
|
200
|
+
for (let i = 0; i < 3; i += 1) {
|
|
201
|
+
const diff = (pa[i] ?? 0) - (pb[i] ?? 0);
|
|
202
|
+
if (diff !== 0)
|
|
203
|
+
return diff;
|
|
204
|
+
}
|
|
205
|
+
return 0;
|
|
206
|
+
}
|
|
197
207
|
/**
|
|
198
208
|
* Format a user-facing warning line for an out-of-range version probe.
|
|
199
209
|
* Setup itself may complete, but the execution-plan gate will refuse model
|
|
200
210
|
* spawns until the operator selects a certified binary or updates the plugin.
|
|
211
|
+
* `nativeV2Certified` is the exact native-v2 table; both engines are named so
|
|
212
|
+
* the newest certified version is not misreported as the legacy-v1 bound.
|
|
201
213
|
*/
|
|
202
|
-
export function formatVersionOutOfRangeWarning(probe, pluginVersion) {
|
|
214
|
+
export function formatVersionOutOfRangeWarning(probe, pluginVersion, nativeV2Certified = []) {
|
|
203
215
|
const tested = KIMI_TESTED_MINORS.map((entry) => `${entry.major}.${entry.minor}.x`).join(", ");
|
|
204
216
|
const max = maxTestedMinor();
|
|
205
|
-
const
|
|
217
|
+
const newestNative = [...nativeV2Certified].sort(compareVersions).at(-1);
|
|
218
|
+
const probeRaw = `${probe.major}.${probe.minor}.${probe.patch}`;
|
|
219
|
+
const aboveMax = newestNative !== undefined
|
|
220
|
+
? compareVersions(probeRaw, newestNative) > 0
|
|
221
|
+
: probe.major > max.major || (probe.major === max.major && probe.minor > max.minor);
|
|
222
|
+
const certified = nativeV2Certified.length > 0
|
|
223
|
+
? `legacy-v1 ${tested}; native-v2 exact ${nativeV2Certified.join(", ")}`
|
|
224
|
+
: tested;
|
|
206
225
|
const lines = [
|
|
207
|
-
`WARNING: kimi-code version ${probe.version} is outside the
|
|
226
|
+
`WARNING: kimi-code version ${probe.version} is outside the versions kimi-plugin-cc ${pluginVersion} certifies (${certified}).`,
|
|
208
227
|
];
|
|
209
228
|
if (aboveMax) {
|
|
210
|
-
//
|
|
211
|
-
|
|
212
|
-
lines.push(` This is NEWER than the newest version we have tested (${max.major}.${max.minor}.x) and is not yet certified for model execution.`);
|
|
229
|
+
// Commonly out-of-band auto-upgrade drift past the last certification.
|
|
230
|
+
lines.push(` This is NEWER than the newest certified version (${newestNative ?? `${max.major}.${max.minor}.x`}) and is not yet certified for model execution.`);
|
|
213
231
|
}
|
|
214
|
-
lines.push(` Setup can complete, but model-spawning commands will refuse before Kimi starts rather than run an unreviewed engine/version route.`, ` Update kimi-plugin-cc to a release that certifies this
|
|
232
|
+
lines.push(` Setup can complete, but model-spawning commands will refuse before Kimi starts rather than run an unreviewed engine/version route.`, ` Update kimi-plugin-cc to a release that certifies this version, or point KIMI_PLUGIN_CC_KIMI_BIN at a certified kimi-code binary and retry setup.`, ` KIMI_PLUGIN_CC_SKIP_VERSION_PROBE is a test/smoke seam, not a production compatibility override.`);
|
|
215
233
|
return lines.join("\n");
|
|
216
234
|
}
|
package/dist/version.js
CHANGED
|
@@ -8,9 +8,11 @@ no separate CLI.
|
|
|
8
8
|
|
|
9
9
|
## Publishing status
|
|
10
10
|
|
|
11
|
-
Plugin **2.5.
|
|
11
|
+
Plugin **2.5.5** keeps every host's managed hook block contiguous in the shared `~/.kimi-code/config.toml`, so a comment-preserving third-party writer that regroups tables cannot strip a block's BEGIN marker, and scopes orphan-marker verification per host: another host's orphan warns, own and legacy orphans still fail closed, and the remedy is plain setup. Exact CLI certification remains 2.5.0 and 2.5.1; publication does not update installed hosts or operator pins.
|
|
12
12
|
|
|
13
|
-
Plugin **2.5.
|
|
13
|
+
Plugin **2.5.4** restores the original platform-neutral bounded-close settlement for swarm agent-state verification after follow-up CI showed that `wasClean` can both accept an early close and reject a valid one. The immutable 2.5.3 artifact contains that trial condition; 2.5.4 carries its revert with deterministic no-close-event coverage and a protocol-correct mock handshake, without changing exact CLI certification through 2.5.1. Activation validation passed 71 tests with 244 assertions, the full repository check passed 1,412 tests with 18,282 assertions, and 330-file package and consumer-security validation passed. [Release v2.5.4](https://github.com/linxule/kimi-plugin-cc/releases/tag/v2.5.4) is anchored at `836da262ae303f51c9049b2711c9d957d7692ed2`. [Release-commit CI](https://github.com/linxule/kimi-plugin-cc/actions/runs/42249517714) and [OIDC publication](https://github.com/linxule/kimi-plugin-cc/actions/runs/42251575637) passed. The 330-file registry tarball has SHA-1 `b9e23fe6b1ceb54fdc5ee26447da050653526b7f`, matches the validated workflow artifact byte-for-byte, and carries provenance (sigstore log index `3721306580`). A fresh registry npm installation has zero production advisories; isolated Claude Code, Codex, and DSH setup/checks pass, and all compiled runtime mirrors match. The Discover timestamp records npm publication at `2026-11-06T16:29:28.637Z`.
|
|
14
|
+
|
|
15
|
+
Plugin **2.5.3** certifies exact Kimi Code **2.5.0 and 2.5.1** for all eight operations, adding local-execution/workspace-trust closures and authenticated WebSocket swarm activation with deterministic close-timeout coverage while preserving the no-plan construction and legacy-v1 0.41.x boundary. See the [2.5.0](./upstream-2.5.0-certification.md) and [2.5.1](./upstream-2.5.1-certification.md) certification records. The 2.5.1 source scan passed 18 tests with 153 assertions; candidate and production matrices each passed 24 tests with 167 assertions and no skips. The full check passed 1,412 tests with 18,282 assertions, package validation passed 330 files across all three distributions, isolated consumer security passed, and independent read-only `claude -p` review returned APPROVE. [Release v2.5.3](https://github.com/linxule/kimi-plugin-cc/releases/tag/v2.5.3) is anchored at `11d862a3fd9dae006c6888bfa221ea73101fb171`. [Final release-commit CI](https://github.com/linxule/kimi-plugin-cc/actions/runs/42237176295) and [OIDC publication](https://github.com/linxule/kimi-plugin-cc/actions/runs/42238045923) passed. The 330-file registry tarball has SHA-1 `6aa5ab4a2c862347e14354698b2ba6f7f8b1defd`, matches the validated workflow artifact byte-for-byte, and carries provenance (sigstore log index `3719667960`). A fresh registry npm installation has zero production advisories; isolated Claude Code, Codex, and DSH setup/checks pass, and all compiled runtime mirrors match. The Discover timestamp records npm publication at `2026-11-06T15:01:13.468Z`. Follow-up CI showed that `wasClean` is inconsistent and has been reverted on `main`; the published 2.5.3 artifact contains the trial condition. [Plugin 2.5.4](https://github.com/linxule/kimi-plugin-cc/releases/tag/v2.5.4) carries the correction.
|
|
14
16
|
|
|
15
17
|
Plugin **2.5.2** hardens setup recovery and DSH dependency ownership. Its real Kimi Code **2.4.1** smoke passed **24 tests and 167 assertions with no skips**; the exact-source audit passed **16 tests and 97 assertions**. Normal npm installation keeps DSH host services optional. DSH use requires explicitly patched host/profile dependencies and both installation audits described below. Exact CLI certification remains unchanged. [Release v2.5.2](https://github.com/linxule/kimi-plugin-cc/releases/tag/v2.5.2) is anchored at `8b303bb6e49c581ad96c72bd031e8fd1643c9121`. [Release-commit CI](https://github.com/linxule/kimi-plugin-cc/actions/runs/41361753223) and [npm publication](https://github.com/linxule/kimi-plugin-cc/actions/runs/41362262009) passed. The 328-file registry tarball has SHA-1 `e620040dd35986a6929be7ad677642729a3126e7`, matches the validated workflow artifact byte-for-byte, and carries provenance (sigstore log index `3636981979`). A fresh registry npm installation has zero production advisories; isolated Claude Code, Codex, and DSH setup/checks pass, and all compiled runtime mirrors match. The Discover timestamp records npm publication at `2026-11-01T23:35:04.270Z`.
|
|
16
18
|
|
|
@@ -48,3 +48,5 @@ After the original certification, Linux release CI exposed a platform-specific e
|
|
|
48
48
|
A subsequent CI attempt showed that native WebSocket close scheduling and `wasClean` semantics vary by platform and cannot supply a deterministic no-close-event precondition. That timeout case now uses a controlled socket which completes the protocol exchange but never emits `close`, and asserts the exact `Kimi web agent-state verification timed out.` message. The refusal is therefore tested without relying on incidental host close timing; production behavior is unchanged by this test-only follow-up.
|
|
49
49
|
|
|
50
50
|
**Follow-up correction:** a later normal activation/deactivation fixture showed that `wasClean` can also reject a valid close. The production condition has been reverted on `main`, restoring the original certified bounded-close settlement; the controlled no-close-event test and protocol-correct mock handshake remain. The immutable 2.5.3 artifact contains the trial condition, so carrying this revert requires a new patch artifact rather than a re-tag or republish.
|
|
51
|
+
|
|
52
|
+
The correction ships as plugin 2.5.4 ([release](https://github.com/linxule/kimi-plugin-cc/releases/tag/v2.5.4)), with registry artifact and fresh-install verification recorded in [registry releases](registry-releases.md).
|
|
@@ -43,3 +43,5 @@ After the original certification and independent review, Linux release CI expose
|
|
|
43
43
|
A subsequent CI attempt showed that native WebSocket close scheduling and `wasClean` semantics vary by platform and cannot supply a deterministic no-close-event precondition. That timeout case now uses a controlled socket which completes the protocol exchange but never emits `close`, and asserts the exact `Kimi web agent-state verification timed out.` message. The refusal is therefore tested without relying on incidental host close timing; production behavior is unchanged by this test-only follow-up.
|
|
44
44
|
|
|
45
45
|
**Follow-up correction:** a later normal activation/deactivation fixture showed that `wasClean` can also reject a valid close. The production condition has been reverted on `main`, restoring the original certified bounded-close settlement; the controlled no-close-event test and protocol-correct mock handshake remain. The immutable 2.5.3 artifact contains the trial condition, so carrying this revert requires a new patch artifact rather than a re-tag or republish.
|
|
46
|
+
|
|
47
|
+
The correction ships as plugin 2.5.4 ([release](https://github.com/linxule/kimi-plugin-cc/releases/tag/v2.5.4)), with registry artifact and fresh-install verification recorded in [registry releases](registry-releases.md).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kimi-plugin-cc",
|
|
3
|
-
"version": "2.5.
|
|
3
|
+
"version": "2.5.5",
|
|
4
4
|
"description": "Kimi for Claude Code, Codex, and DeepSeek Harness: code questions, independent reviews, bounded fixes, autonomous goals, parallel tasks, and MCP tools through the local Kimi Code CLI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/dsh/index.js",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kimi",
|
|
3
|
-
"version": "2.5.
|
|
3
|
+
"version": "2.5.5",
|
|
4
4
|
"description": "Kimi for Codex: code questions, independent reviews, bounded fixes, autonomous goals, parallel tasks, and MCP job controls through the local Kimi Code CLI.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "linxule"
|
|
@@ -26,9 +26,9 @@ import { readPluginConfig, writePluginConfig } from "../config.js";
|
|
|
26
26
|
import { RuntimeError } from "../errors.js";
|
|
27
27
|
import { normalizeTopLevelInlineHooks, validateKimiHookSetForEnvironment, withKimiConfigLock, } from "../hooks/config-safety.js";
|
|
28
28
|
import { buildHookShellCommand, hostIdFromHookCommand, resolveHookScriptPath, resolveHostId, resolveNodeBinary, } from "../hooks/install-paths.js";
|
|
29
|
-
import { decodeManagedCommandLine, effectiveHost, evaluateInstalled, findUnmanagedApprovalHookBlocks, markerCommentLines, MARKERS, parseManagedBlock, } from "../hooks/managed-block.js";
|
|
29
|
+
import { decodeManagedCommandLine, effectiveHost, evaluateInstalled, findUnmanagedApprovalHookBlocks, markerCommentLines, MARKERS, ORPHAN_MARKER_REMEDY, parseManagedBlock, } from "../hooks/managed-block.js";
|
|
30
30
|
import { formatVersionOutOfRangeWarning, probeKimiVersion, } from "../kimi-version-probe.js";
|
|
31
|
-
import { isNativeV2CertifiedVersion } from "../kimi-engine.js";
|
|
31
|
+
import { isNativeV2CertifiedVersion, NATIVE_V2_CERTIFIED_VERSIONS } from "../kimi-engine.js";
|
|
32
32
|
import { resolveKimiHome } from "../kimi-home.js";
|
|
33
33
|
import { ensurePluginPaths, resolvePluginPaths } from "../paths.js";
|
|
34
34
|
import { KIMI_PLUGIN_CC_VERSION } from "../version.js";
|
|
@@ -180,7 +180,7 @@ async function runInstall(configPath, hookScriptPath, hostId, reviewGateEnabled,
|
|
|
180
180
|
const block = buildManagedBlock(hookScriptPath, context.env, lineEnding, hostId);
|
|
181
181
|
const next = state.kind === "found"
|
|
182
182
|
? spliceBlock(existing, state.beginLine, state.endLine, block, lineEnding)
|
|
183
|
-
:
|
|
183
|
+
: insertBlockNearManagedBlocks(existing, blocks, block, lineEnding);
|
|
184
184
|
await assertKimiHookSetValid(next, configPath, "setup.install", context.env);
|
|
185
185
|
const blockWritten = next !== original;
|
|
186
186
|
if (blockWritten)
|
|
@@ -256,7 +256,10 @@ async function runCheck(configPath, hookScriptPath, hostId, reviewGateEnabled, w
|
|
|
256
256
|
nodeExists: (binPath) => existsSync(binPath),
|
|
257
257
|
});
|
|
258
258
|
// Non-blocking coexistence + cruft notices.
|
|
259
|
-
const { blocks: allBlocks } = parseManagedBlock(existing, hostId);
|
|
259
|
+
const { blocks: allBlocks, foreignOrphans } = parseManagedBlock(existing, hostId);
|
|
260
|
+
for (const orphan of foreignOrphans) {
|
|
261
|
+
warnings.push(`Host "${orphan.hostId}" has an orphaned ${orphan.detail} managed marker at line ${orphan.line + 1}; that host's commands refuse until it is repaired. ${ORPHAN_MARKER_REMEDY}`);
|
|
262
|
+
}
|
|
260
263
|
const otherHosts = [
|
|
261
264
|
...new Set(allBlocks.map((b) => effectiveHost(b, hostId)).filter((h) => h !== hostId)),
|
|
262
265
|
];
|
|
@@ -303,7 +306,9 @@ async function runCheck(configPath, hookScriptPath, hostId, reviewGateEnabled, w
|
|
|
303
306
|
reviewGateEnabled,
|
|
304
307
|
nextStep: installedCheck.state.kind === "absent"
|
|
305
308
|
? "Run /kimi:setup (without --check) to install the managed block."
|
|
306
|
-
:
|
|
309
|
+
: installedCheck.state.kind === "orphan"
|
|
310
|
+
? ORPHAN_MARKER_REMEDY
|
|
311
|
+
: "Run /kimi:setup --uninstall, then /kimi:setup to repair.",
|
|
307
312
|
details: buildDetails({
|
|
308
313
|
configPath,
|
|
309
314
|
hookScriptPath,
|
|
@@ -535,6 +540,52 @@ function spliceBlock(contents, beginLine, endLine, replacement, lineEnding) {
|
|
|
535
540
|
const replacementWithEnding = replacement.endsWith(lineEnding) ? replacement : `${replacement}${lineEnding}`;
|
|
536
541
|
return `${beforeWithSep}${replacementWithEnding}${after}`;
|
|
537
542
|
}
|
|
543
|
+
// Keep every host's block in one contiguous run. Magpie's table rewrite drops
|
|
544
|
+
// comments that follow a table it owns (yetone/magpie#5614): a block appended
|
|
545
|
+
// after such a table loses its BEGIN marker and header, leaving an orphan END.
|
|
546
|
+
// Insert after the last managed END only when nothing but blanks/comments
|
|
547
|
+
// precedes the next header AND the real parser proves the result equals the
|
|
548
|
+
// appended form apart from hook order, so no field can rebind and no value can
|
|
549
|
+
// be split; otherwise append.
|
|
550
|
+
function insertBlockNearManagedBlocks(contents, blocks, block, lineEnding) {
|
|
551
|
+
if (blocks.length === 0)
|
|
552
|
+
return appendBlock(contents, block, lineEnding);
|
|
553
|
+
const anchor = Math.max(...blocks.map((b) => b.endLine));
|
|
554
|
+
const lines = splitPreservingEnding(contents);
|
|
555
|
+
for (let i = anchor + 1; i < lines.length; i += 1) {
|
|
556
|
+
const trimmed = lines[i].trim();
|
|
557
|
+
if (trimmed.startsWith("["))
|
|
558
|
+
break;
|
|
559
|
+
if (trimmed.length > 0 && !trimmed.startsWith("#"))
|
|
560
|
+
return appendBlock(contents, block, lineEnding);
|
|
561
|
+
}
|
|
562
|
+
const rawLines = contents.split(/(?<=\n)/);
|
|
563
|
+
const head = rawLines.slice(0, anchor + 1).join("");
|
|
564
|
+
const tail = rawLines.slice(anchor + 1).join("");
|
|
565
|
+
const appended = appendBlock(contents, block, lineEnding);
|
|
566
|
+
if (tail.length === 0)
|
|
567
|
+
return appended;
|
|
568
|
+
const headWithEnding = head.endsWith("\n") ? head : `${head}${lineEnding}`;
|
|
569
|
+
const body = block.endsWith(lineEnding) ? block : `${block}${lineEnding}`;
|
|
570
|
+
const tailSeparator = /^\r?\n/.test(tail) ? "" : lineEnding;
|
|
571
|
+
const inserted = `${headWithEnding}${lineEnding}${body}${tailSeparator}${tail}`;
|
|
572
|
+
return sameTomlIgnoringHookOrder(inserted, appended) ? inserted : appended;
|
|
573
|
+
}
|
|
574
|
+
function sameTomlIgnoringHookOrder(a, b) {
|
|
575
|
+
const canonical = (text) => {
|
|
576
|
+
const parsed = parseToml(text);
|
|
577
|
+
const hooks = Array.isArray(parsed.hooks)
|
|
578
|
+
? parsed.hooks.map((entry) => JSON.stringify(entry)).sort()
|
|
579
|
+
: parsed.hooks;
|
|
580
|
+
return JSON.stringify({ ...parsed, hooks });
|
|
581
|
+
};
|
|
582
|
+
try {
|
|
583
|
+
return canonical(a) === canonical(b);
|
|
584
|
+
}
|
|
585
|
+
catch {
|
|
586
|
+
return false;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
538
589
|
function appendBlock(contents, block, lineEnding) {
|
|
539
590
|
const base = contents.length === 0 || contents.endsWith(lineEnding) ? contents : `${contents}${lineEnding}`;
|
|
540
591
|
const separator = base.length === 0 ? "" : lineEnding;
|
|
@@ -928,7 +979,7 @@ async function collectKimiVersionWarnings(env, warnings) {
|
|
|
928
979
|
// v2-certified binary would wrongly warn "not certified, commands will refuse".
|
|
929
980
|
if (probe.inTestedRange || isNativeV2CertifiedVersion(probe.version))
|
|
930
981
|
return;
|
|
931
|
-
warnings.push(formatVersionOutOfRangeWarning(probe, KIMI_PLUGIN_CC_VERSION));
|
|
982
|
+
warnings.push(formatVersionOutOfRangeWarning(probe, KIMI_PLUGIN_CC_VERSION, NATIVE_V2_CERTIFIED_VERSIONS));
|
|
932
983
|
}
|
|
933
984
|
/**
|
|
934
985
|
* Non-blocking notice for kimi-code's own enabled plugins from
|