kimi-plugin-cc 2.5.1 → 2.5.3
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 +15 -1
- package/README.md +62 -4
- package/dist/cli-client.js +8 -0
- package/dist/commands/setup.js +78 -51
- package/dist/commands/swarm.js +2 -2
- package/dist/dsh/companion.js +9 -6
- package/dist/dsh/index.js +15 -2
- package/dist/hooks/config-safety.js +5 -2
- package/dist/hooks/managed-block.js +86 -20
- package/dist/kimi-command.js +1 -0
- package/dist/kimi-engine.js +3 -0
- package/dist/native-v2-preflight.js +138 -30
- package/dist/swarm-activation.js +276 -14
- package/dist/version.js +1 -1
- package/docs/migration.md +8 -6
- package/docs/registry-releases.md +66 -3
- package/docs/safety.md +14 -10
- package/docs/swarm-recovery.md +2 -2
- package/docs/upstream-2.5.0-certification.md +48 -0
- package/docs/upstream-2.5.1-certification.md +43 -0
- package/package.json +40 -5
- package/plugins/kimi-codex/.codex-plugin/plugin.json +1 -1
- package/plugins/kimi-codex/dist/cli-client.js +8 -0
- package/plugins/kimi-codex/dist/commands/setup.js +78 -51
- package/plugins/kimi-codex/dist/commands/swarm.js +2 -2
- package/plugins/kimi-codex/dist/dsh/companion.js +9 -6
- package/plugins/kimi-codex/dist/dsh/index.js +15 -2
- package/plugins/kimi-codex/dist/hooks/config-safety.js +5 -2
- package/plugins/kimi-codex/dist/hooks/managed-block.js +86 -20
- package/plugins/kimi-codex/dist/kimi-command.js +1 -0
- package/plugins/kimi-codex/dist/kimi-engine.js +3 -0
- package/plugins/kimi-codex/dist/native-v2-preflight.js +138 -30
- package/plugins/kimi-codex/dist/swarm-activation.js +276 -14
- package/plugins/kimi-codex/dist/version.js +1 -1
- package/plugins/kimi-dsh/README.md +4 -1
- package/plugins/kimi-dsh/dist/cli-client.js +8 -0
- package/plugins/kimi-dsh/dist/commands/setup.js +78 -51
- package/plugins/kimi-dsh/dist/commands/swarm.js +2 -2
- package/plugins/kimi-dsh/dist/dsh/companion.js +9 -6
- package/plugins/kimi-dsh/dist/dsh/index.js +15 -2
- package/plugins/kimi-dsh/dist/hooks/config-safety.js +5 -2
- package/plugins/kimi-dsh/dist/hooks/managed-block.js +86 -20
- package/plugins/kimi-dsh/dist/kimi-command.js +1 -0
- package/plugins/kimi-dsh/dist/kimi-engine.js +3 -0
- package/plugins/kimi-dsh/dist/native-v2-preflight.js +138 -30
- package/plugins/kimi-dsh/dist/swarm-activation.js +276 -14
- package/plugins/kimi-dsh/dist/version.js +1 -1
- package/plugins/kimi-dsh/package.json +33 -3
|
@@ -9,7 +9,8 @@ import { SETUP_SUPPORTED_FLAGS } from "../parsing.js";
|
|
|
9
9
|
// absolute Node path avoids PATH dependence under sanitized launches. Setup
|
|
10
10
|
// probes the hook directly with resolveNodeBinary and through `/bin/sh -c`
|
|
11
11
|
// using the exact installed command bytes. It also warns about broad permission
|
|
12
|
-
// denies
|
|
12
|
+
// denies, repairs orphan marker comments, and refuses remaining
|
|
13
|
+
// duplicate, orphaned, or malformed managed blocks.
|
|
13
14
|
//
|
|
14
15
|
// Subcommands: install (default), --check, --uninstall [--all], and
|
|
15
16
|
// --enable-review-gate/--disable-review-gate.
|
|
@@ -25,12 +26,13 @@ import { readPluginConfig, writePluginConfig } from "../config.js";
|
|
|
25
26
|
import { RuntimeError } from "../errors.js";
|
|
26
27
|
import { normalizeTopLevelInlineHooks, validateKimiHookSetForEnvironment, withKimiConfigLock, } from "../hooks/config-safety.js";
|
|
27
28
|
import { buildHookShellCommand, hostIdFromHookCommand, resolveHookScriptPath, resolveHostId, resolveNodeBinary, } from "../hooks/install-paths.js";
|
|
28
|
-
import { decodeManagedCommandLine, effectiveHost, evaluateInstalled, findUnmanagedApprovalHookBlocks, MARKERS, parseManagedBlock, } from "../hooks/managed-block.js";
|
|
29
|
+
import { decodeManagedCommandLine, effectiveHost, evaluateInstalled, findUnmanagedApprovalHookBlocks, markerCommentLines, MARKERS, parseManagedBlock, } from "../hooks/managed-block.js";
|
|
29
30
|
import { formatVersionOutOfRangeWarning, probeKimiVersion, } from "../kimi-version-probe.js";
|
|
30
31
|
import { isNativeV2CertifiedVersion } from "../kimi-engine.js";
|
|
31
32
|
import { resolveKimiHome } from "../kimi-home.js";
|
|
32
33
|
import { ensurePluginPaths, resolvePluginPaths } from "../paths.js";
|
|
33
34
|
import { KIMI_PLUGIN_CC_VERSION } from "../version.js";
|
|
35
|
+
import { parse as parseToml } from "../vendor/smol-toml/parse.js";
|
|
34
36
|
const BEGIN_MARKER_PREFIX = "# === BEGIN kimi-plugin-cc-managed";
|
|
35
37
|
const DEFAULT_HOOK_TIMEOUT_S = 15;
|
|
36
38
|
// Probe timing. The direct script probe is local and fast; allow 5s for
|
|
@@ -119,7 +121,17 @@ async function runInstall(configPath, hookScriptPath, hostId, reviewGateEnabled,
|
|
|
119
121
|
const mutation = await withKimiConfigLock(configPath, async () => {
|
|
120
122
|
const original = await readConfigSafe(configPath);
|
|
121
123
|
const lineEnding = detectLineEnding(original);
|
|
122
|
-
|
|
124
|
+
try {
|
|
125
|
+
parseToml(original);
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
await assertKimiHookSetValid(original, configPath, "setup.install", context.env);
|
|
129
|
+
}
|
|
130
|
+
const repaired = removeOrphanMarkers(original);
|
|
131
|
+
if (repaired.count > 0) {
|
|
132
|
+
warnings.push(`Removed ${repaired.count} orphaned managed marker comment(s); surrounding config was preserved.`);
|
|
133
|
+
}
|
|
134
|
+
const inlineHooks = normalizeTopLevelInlineHooks(repaired.contents, lineEnding);
|
|
123
135
|
if (inlineHooks.found) {
|
|
124
136
|
// Validate before normalizing so an invalid inline entry is reported
|
|
125
137
|
// against the user's original bytes and the config remains untouched.
|
|
@@ -133,7 +145,7 @@ async function runInstall(configPath, hookScriptPath, hostId, reviewGateEnabled,
|
|
|
133
145
|
// splice line numbers. Another host's marker-less table may be its live hook
|
|
134
146
|
// after a config rewrite strips comments, so it must remain untouched.
|
|
135
147
|
const expectedCommand = buildHookShellCommand(hookScriptPath, context.env);
|
|
136
|
-
const { pruned, count: prunedCount, commands: prunedCommands } = pruneOrphanApprovalHooks(inlineHooks.contents,
|
|
148
|
+
const { pruned, count: prunedCount, commands: prunedCommands } = pruneOrphanApprovalHooks(inlineHooks.contents, hostId, expectedCommand);
|
|
137
149
|
const existing = pruned;
|
|
138
150
|
if (prunedCount > 0) {
|
|
139
151
|
const readorned = prunedCommands.includes(expectedCommand);
|
|
@@ -145,7 +157,7 @@ async function runInstall(configPath, hookScriptPath, hostId, reviewGateEnabled,
|
|
|
145
157
|
if (state.kind === "orphan") {
|
|
146
158
|
throw new RuntimeError("SETUP_ORPHAN_MARKERS", [
|
|
147
159
|
`kimi-code config at ${configPath} contains an orphaned ${state.detail} marker.`,
|
|
148
|
-
"
|
|
160
|
+
"Inspect the managed marker comments and re-run setup.",
|
|
149
161
|
].join(" "), "setup.install", { details: { configPath, orphan: state.detail } });
|
|
150
162
|
}
|
|
151
163
|
if (state.kind === "duplicate") {
|
|
@@ -393,12 +405,11 @@ async function runUninstallLocked(configPath, hookScriptPath, hostId, removeAllH
|
|
|
393
405
|
// other hosts' blocks and live marker-less tables remain intact. `--all` is
|
|
394
406
|
// the explicit every-host sweep for all of this plugin's hook tables.
|
|
395
407
|
const { stripped: strippedMarkers, removedBlocks, orphansLeft } = stripManagedBlocks(existing, hostId, removeAllHosts);
|
|
396
|
-
const lineEnding = detectLineEnding(existing);
|
|
397
408
|
// Exact current command bytes also identify this host's table when a host-id
|
|
398
409
|
// override disagrees with path derivation or a dev path is outside the strict
|
|
399
410
|
// ownership pattern.
|
|
400
411
|
const expectedCommand = buildHookShellCommand(hookScriptPath, context.env);
|
|
401
|
-
const { pruned: stripped, count: prunedCount } = pruneOrphanApprovalHooks(strippedMarkers,
|
|
412
|
+
const { pruned: stripped, count: prunedCount } = pruneOrphanApprovalHooks(strippedMarkers, removeAllHosts ? undefined : hostId, expectedCommand);
|
|
402
413
|
const changed = stripped !== existing;
|
|
403
414
|
if (changed) {
|
|
404
415
|
await writeConfigAtomic(configPath, stripped);
|
|
@@ -517,10 +528,10 @@ function detectLineEnding(contents) {
|
|
|
517
528
|
return contents.includes("\r\n") ? "\r\n" : "\n";
|
|
518
529
|
}
|
|
519
530
|
function spliceBlock(contents, beginLine, endLine, replacement, lineEnding) {
|
|
520
|
-
const lines =
|
|
521
|
-
const before = lines.slice(0, beginLine).join(
|
|
522
|
-
const after = lines.slice(endLine + 1).join(
|
|
523
|
-
const beforeWithSep = before.endsWith(
|
|
531
|
+
const lines = contents.split(/(?<=\n)/);
|
|
532
|
+
const before = lines.slice(0, beginLine).join("");
|
|
533
|
+
const after = lines.slice(endLine + 1).join("");
|
|
534
|
+
const beforeWithSep = before.endsWith("\n") || before.length === 0 ? before : `${before}${lineEnding}`;
|
|
524
535
|
const replacementWithEnding = replacement.endsWith(lineEnding) ? replacement : `${replacement}${lineEnding}`;
|
|
525
536
|
return `${beforeWithSep}${replacementWithEnding}${after}`;
|
|
526
537
|
}
|
|
@@ -536,14 +547,54 @@ function splitPreservingEnding(contents) {
|
|
|
536
547
|
// pass the desired line ending).
|
|
537
548
|
return contents.split("\n").map((line) => line.replace(/\r$/, ""));
|
|
538
549
|
}
|
|
550
|
+
// A partial upstream hooks-array rewrite can leave one marker comment behind.
|
|
551
|
+
// Remove only unpaired marker comments, never the intervening content. The
|
|
552
|
+
// normal full-file validation still gates writes.
|
|
553
|
+
function removeOrphanMarkers(contents) {
|
|
554
|
+
const lines = splitPreservingEnding(contents);
|
|
555
|
+
const orphans = [];
|
|
556
|
+
const comments = markerCommentLines(contents);
|
|
557
|
+
let open = null;
|
|
558
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
559
|
+
if (!comments.has(i))
|
|
560
|
+
continue;
|
|
561
|
+
const line = lines[i].trim();
|
|
562
|
+
const begin = MARKERS.BEGIN_LINE_RE.exec(line);
|
|
563
|
+
if (begin !== null) {
|
|
564
|
+
if (open !== null)
|
|
565
|
+
orphans.push(open);
|
|
566
|
+
open = { index: i, host: begin[1]?.toLowerCase() ?? null };
|
|
567
|
+
}
|
|
568
|
+
else {
|
|
569
|
+
const end = MARKERS.END_LINE_RE.exec(line);
|
|
570
|
+
if (end === null)
|
|
571
|
+
continue;
|
|
572
|
+
const endMarker = { index: i, host: end[1]?.toLowerCase() ?? null };
|
|
573
|
+
if (open === null)
|
|
574
|
+
orphans.push(endMarker);
|
|
575
|
+
else {
|
|
576
|
+
if (open.host !== endMarker.host)
|
|
577
|
+
orphans.push(open, endMarker);
|
|
578
|
+
open = null;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
if (open !== null)
|
|
583
|
+
orphans.push(open);
|
|
584
|
+
const remove = new Set(orphans.map((marker) => marker.index));
|
|
585
|
+
return remove.size === 0
|
|
586
|
+
? { contents, count: 0 }
|
|
587
|
+
: { contents: contents.split(/(?<=\n)/).filter((_, index) => !remove.has(index)).join(""), count: remove.size };
|
|
588
|
+
}
|
|
539
589
|
/**
|
|
540
590
|
* Remove managed blocks owned by `currentHost`, or every host with `removeAll`.
|
|
541
591
|
* Ownership comes from the marker suffix or a legacy command's derived host.
|
|
542
592
|
* Orphan markers are removed individually so surrounding user content survives.
|
|
543
593
|
*/
|
|
544
594
|
function stripManagedBlocks(contents, currentHost, removeAll) {
|
|
545
|
-
const lineEnding = detectLineEnding(contents);
|
|
546
595
|
const lines = splitPreservingEnding(contents);
|
|
596
|
+
const rawLines = contents.split(/(?<=\n)/);
|
|
597
|
+
const markerLines = markerCommentLines(contents);
|
|
547
598
|
const result = [];
|
|
548
599
|
const orphansLeft = [];
|
|
549
600
|
let removedBlocks = 0;
|
|
@@ -567,7 +618,7 @@ function stripManagedBlocks(contents, currentHost, removeAll) {
|
|
|
567
618
|
while (i < lines.length) {
|
|
568
619
|
const line = lines[i];
|
|
569
620
|
const trimmed = line.trim();
|
|
570
|
-
const begin = MARKERS.BEGIN_LINE_RE.exec(trimmed);
|
|
621
|
+
const begin = markerLines.has(i) ? MARKERS.BEGIN_LINE_RE.exec(trimmed) : null;
|
|
571
622
|
if (begin !== null) {
|
|
572
623
|
const beginHost = begin[1]?.toLowerCase() ?? null;
|
|
573
624
|
// Look ahead for the matching END, capturing the block's command en route.
|
|
@@ -575,12 +626,14 @@ function stripManagedBlocks(contents, currentHost, removeAll) {
|
|
|
575
626
|
let blockCommand = null;
|
|
576
627
|
for (let j = i + 1; j < lines.length; j += 1) {
|
|
577
628
|
const nextTrimmed = lines[j].trim();
|
|
578
|
-
if (MARKERS.BEGIN_LINE_RE.test(nextTrimmed)) {
|
|
629
|
+
if (markerLines.has(j) && MARKERS.BEGIN_LINE_RE.test(nextTrimmed)) {
|
|
579
630
|
// Two BEGINs in a row — the outer one is an orphan.
|
|
580
631
|
break;
|
|
581
632
|
}
|
|
582
|
-
|
|
583
|
-
|
|
633
|
+
const end = markerLines.has(j) ? MARKERS.END_LINE_RE.exec(nextTrimmed) : null;
|
|
634
|
+
if (end !== null) {
|
|
635
|
+
if ((end[1]?.toLowerCase() ?? null) === beginHost)
|
|
636
|
+
endIdx = j;
|
|
584
637
|
break;
|
|
585
638
|
}
|
|
586
639
|
const decoded = decodeManagedCommandLine(nextTrimmed);
|
|
@@ -594,7 +647,7 @@ function stripManagedBlocks(contents, currentHost, removeAll) {
|
|
|
594
647
|
i += 1;
|
|
595
648
|
continue;
|
|
596
649
|
}
|
|
597
|
-
result.push(
|
|
650
|
+
result.push(rawLines[i]);
|
|
598
651
|
i += 1;
|
|
599
652
|
continue;
|
|
600
653
|
}
|
|
@@ -606,11 +659,11 @@ function stripManagedBlocks(contents, currentHost, removeAll) {
|
|
|
606
659
|
}
|
|
607
660
|
// Another host's block — keep it verbatim.
|
|
608
661
|
for (let k = i; k <= endIdx; k += 1)
|
|
609
|
-
result.push(
|
|
662
|
+
result.push(rawLines[k]);
|
|
610
663
|
i = endIdx + 1;
|
|
611
664
|
continue;
|
|
612
665
|
}
|
|
613
|
-
const end = MARKERS.END_LINE_RE.exec(trimmed);
|
|
666
|
+
const end = markerLines.has(i) ? MARKERS.END_LINE_RE.exec(trimmed) : null;
|
|
614
667
|
if (end !== null) {
|
|
615
668
|
const endHost = end[1]?.toLowerCase() ?? null;
|
|
616
669
|
// Orphan END with no preceding BEGIN: drop only this line (if ours).
|
|
@@ -619,27 +672,14 @@ function stripManagedBlocks(contents, currentHost, removeAll) {
|
|
|
619
672
|
i += 1;
|
|
620
673
|
continue;
|
|
621
674
|
}
|
|
622
|
-
result.push(
|
|
675
|
+
result.push(rawLines[i]);
|
|
623
676
|
i += 1;
|
|
624
677
|
continue;
|
|
625
678
|
}
|
|
626
|
-
result.push(
|
|
679
|
+
result.push(rawLines[i]);
|
|
627
680
|
i += 1;
|
|
628
681
|
}
|
|
629
|
-
|
|
630
|
-
let blankRun = 0;
|
|
631
|
-
for (const line of result) {
|
|
632
|
-
if (line.length === 0) {
|
|
633
|
-
blankRun += 1;
|
|
634
|
-
if (blankRun <= 2)
|
|
635
|
-
collapsed.push(line);
|
|
636
|
-
}
|
|
637
|
-
else {
|
|
638
|
-
blankRun = 0;
|
|
639
|
-
collapsed.push(line);
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
return { stripped: collapsed.join(lineEnding), removedBlocks, orphansLeft };
|
|
682
|
+
return { stripped: result.join(""), removedBlocks, orphansLeft };
|
|
643
683
|
}
|
|
644
684
|
/**
|
|
645
685
|
* Remove orphaned, marker-less `[[hooks]]` tables that are unambiguously this
|
|
@@ -654,7 +694,7 @@ function stripManagedBlocks(contents, currentHost, removeAll) {
|
|
|
654
694
|
* host — REQUIRED for scoped install/uninstall so one host never disarms the
|
|
655
695
|
* other's live (marker-stripped) hook. Omit only for `--uninstall --all`.
|
|
656
696
|
*/
|
|
657
|
-
function pruneOrphanApprovalHooks(contents,
|
|
697
|
+
function pruneOrphanApprovalHooks(contents, ownedBy, alsoMatchCommand) {
|
|
658
698
|
const ranges = findUnmanagedApprovalHookBlocks(contents, ownedBy, alsoMatchCommand);
|
|
659
699
|
if (ranges.length === 0)
|
|
660
700
|
return { pruned: contents, count: 0, commands: [] };
|
|
@@ -663,23 +703,10 @@ function pruneOrphanApprovalHooks(contents, lineEnding, ownedBy, alsoMatchComman
|
|
|
663
703
|
for (let i = start; i < end; i += 1)
|
|
664
704
|
remove.add(i);
|
|
665
705
|
}
|
|
666
|
-
const lines =
|
|
706
|
+
const lines = contents.split(/(?<=\n)/);
|
|
667
707
|
const kept = lines.filter((_, idx) => !remove.has(idx));
|
|
668
|
-
const collapsed = [];
|
|
669
|
-
let blankRun = 0;
|
|
670
|
-
for (const line of kept) {
|
|
671
|
-
if (line.length === 0) {
|
|
672
|
-
blankRun += 1;
|
|
673
|
-
if (blankRun <= 2)
|
|
674
|
-
collapsed.push(line);
|
|
675
|
-
}
|
|
676
|
-
else {
|
|
677
|
-
blankRun = 0;
|
|
678
|
-
collapsed.push(line);
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
708
|
return {
|
|
682
|
-
pruned:
|
|
709
|
+
pruned: kept.join(""),
|
|
683
710
|
count: ranges.length,
|
|
684
711
|
commands: ranges.map((range) => range.command),
|
|
685
712
|
};
|
|
@@ -28,8 +28,8 @@ import { buildKimiSessionTitle, syncKimiSessionTitle } from "../session-title.js
|
|
|
28
28
|
// only the wall-clock hard bound. Read/write modes reuse review/rescue lineage,
|
|
29
29
|
// and the mandatory PreToolUse hook enforces read-only policy or trusted-worktree
|
|
30
30
|
// write confinement on the coordinator and every subagent. Write mode requires
|
|
31
|
-
// kimi-code 0.18.0+. Exact 2.3.2, 2.3.3, 2.4.0
|
|
32
|
-
// session through a bounded loopback web lifecycle, then resume it through the normal print path.
|
|
31
|
+
// kimi-code 0.18.0+. Exact 2.3.2, 2.3.3, 2.4.0, 2.4.1, 2.5.0 and 2.5.1 additionally
|
|
32
|
+
// activate a same-job session through a bounded loopback web lifecycle, then resume it through the normal print path.
|
|
33
33
|
const SWARM_SUMMARY_MAX = 120;
|
|
34
34
|
const SWARM_AGENT_PROFILE = "<swarm>";
|
|
35
35
|
const SWARM_WRITE_AGENT_PROFILE = "<swarm-write>";
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { brandString } from "@deepseek-ai/dsh-brand";
|
|
3
3
|
import { assertUsableCwd, settleRunResult, subprocessRunHandle, } from "@deepseek-ai/dsh-subagent";
|
|
4
|
+
import { preferStableNodePath } from "../hooks/install-paths.js";
|
|
4
5
|
export const DSH_COMPANION_OPERATIONS = [
|
|
5
6
|
"ask",
|
|
6
7
|
"review",
|
|
@@ -20,7 +21,7 @@ export function companionTextTask(prompt) {
|
|
|
20
21
|
}
|
|
21
22
|
return text;
|
|
22
23
|
}
|
|
23
|
-
export function companionArgv(operation, prompt, companionPath, nodeBin = process.execPath, options = {}) {
|
|
24
|
+
export function companionArgv(operation, prompt, companionPath, nodeBin = preferStableNodePath(process.execPath), options = {}) {
|
|
24
25
|
const command = operation === "ask" || operation === "review"
|
|
25
26
|
? [operation]
|
|
26
27
|
: operation === "swarm-write"
|
|
@@ -41,7 +42,7 @@ export function companionArgv(operation, prompt, companionPath, nodeBin = proces
|
|
|
41
42
|
prompt,
|
|
42
43
|
];
|
|
43
44
|
}
|
|
44
|
-
export function companionEnvironment(dataRoot, cwd, env, nodeBin = process.execPath) {
|
|
45
|
+
export function companionEnvironment(dataRoot, cwd, env, nodeBin = preferStableNodePath(process.execPath)) {
|
|
45
46
|
return {
|
|
46
47
|
...env,
|
|
47
48
|
KIMI_PLUGIN_CC_DATA: dataRoot,
|
|
@@ -79,8 +80,9 @@ export async function startCompanionRun(request, operation, spec) {
|
|
|
79
80
|
request.signal.addEventListener("abort", onAbort, { once: true });
|
|
80
81
|
let child;
|
|
81
82
|
try {
|
|
83
|
+
const nodeBin = preferStableNodePath(process.execPath);
|
|
82
84
|
child = spec.spawn({
|
|
83
|
-
argv: companionArgv(operation, prompt, spec.companionPath,
|
|
85
|
+
argv: companionArgv(operation, prompt, spec.companionPath, nodeBin, spec),
|
|
84
86
|
cwd,
|
|
85
87
|
stdio: {
|
|
86
88
|
stdin: "ignore",
|
|
@@ -89,7 +91,7 @@ export async function startCompanionRun(request, operation, spec) {
|
|
|
89
91
|
},
|
|
90
92
|
graceMs: spec.disposeGraceMs,
|
|
91
93
|
signal: termination.signal,
|
|
92
|
-
env: companionEnvironment(spec.dataRoot, cwd, spec.env),
|
|
94
|
+
env: companionEnvironment(spec.dataRoot, cwd, spec.env, nodeBin),
|
|
93
95
|
});
|
|
94
96
|
}
|
|
95
97
|
catch (error) {
|
|
@@ -141,8 +143,9 @@ export async function startCompanionRun(request, operation, spec) {
|
|
|
141
143
|
});
|
|
142
144
|
}
|
|
143
145
|
export async function runCompanionCommand(argv, cwd, spec, signal) {
|
|
146
|
+
const nodeBin = preferStableNodePath(process.execPath);
|
|
144
147
|
const child = spec.spawn({
|
|
145
|
-
argv: [
|
|
148
|
+
argv: [nodeBin, spec.companionPath, ...argv],
|
|
146
149
|
cwd,
|
|
147
150
|
stdio: {
|
|
148
151
|
stdin: "ignore",
|
|
@@ -151,7 +154,7 @@ export async function runCompanionCommand(argv, cwd, spec, signal) {
|
|
|
151
154
|
},
|
|
152
155
|
graceMs: spec.disposeGraceMs,
|
|
153
156
|
signal,
|
|
154
|
-
env: companionEnvironment(spec.dataRoot, cwd, spec.env),
|
|
157
|
+
env: companionEnvironment(spec.dataRoot, cwd, spec.env, nodeBin),
|
|
155
158
|
});
|
|
156
159
|
try {
|
|
157
160
|
const outcome = await child.done;
|
|
@@ -8,6 +8,12 @@ import { DSH_COMPANION_OPERATIONS, runCompanionCommand, startCompanionRun, } fro
|
|
|
8
8
|
import { parseDurationMs } from "../parsing.js";
|
|
9
9
|
export const name = "kimi-plugin-cc";
|
|
10
10
|
export const inject = ["subagents", "subprocess"];
|
|
11
|
+
export const DSH_TOOL_GUIDANCE = [
|
|
12
|
+
"Kimi delegation tools have different write boundaries:",
|
|
13
|
+
"kimi_ask, kimi_review, and kimi_challenge are read-only; kimi_swarm is a read-only parallel review.",
|
|
14
|
+
"kimi_rescue and kimi_pursue can edit the selected workspace; kimi_pursue continues toward a bounded goal.",
|
|
15
|
+
"kimi_swarm_write edits a temporary worktree and returns a patch without applying it.",
|
|
16
|
+
].join(" ");
|
|
11
17
|
const DEFAULT_DISPOSE_GRACE_MS = 6_000;
|
|
12
18
|
const DEFAULT_MAX_OUTPUT_BYTES = 16 * 1024 * 1024;
|
|
13
19
|
const MAX_TIMER_DELAY_MS = 2_147_483_647;
|
|
@@ -50,8 +56,8 @@ function resolveConfig(config) {
|
|
|
50
56
|
if (!path.isAbsolute(resolved.dataRoot)) {
|
|
51
57
|
throw new Error("kimi-plugin-cc: dataRoot must be absolute");
|
|
52
58
|
}
|
|
53
|
-
if (!Number.isFinite(resolved.disposeGraceMs) || resolved.disposeGraceMs
|
|
54
|
-
throw new Error(`kimi-plugin-cc: disposeGraceMs must be
|
|
59
|
+
if (!Number.isFinite(resolved.disposeGraceMs) || resolved.disposeGraceMs < DEFAULT_DISPOSE_GRACE_MS || resolved.disposeGraceMs > MAX_TIMER_DELAY_MS) {
|
|
60
|
+
throw new Error(`kimi-plugin-cc: disposeGraceMs must be at least ${DEFAULT_DISPOSE_GRACE_MS} and no greater than ${MAX_TIMER_DELAY_MS}`);
|
|
55
61
|
}
|
|
56
62
|
if (!Number.isSafeInteger(resolved.maxOutputBytes) || resolved.maxOutputBytes <= 0) {
|
|
57
63
|
throw new Error("kimi-plugin-cc: maxOutputBytes must be a positive safe integer");
|
|
@@ -105,6 +111,13 @@ export function apply(ctx, config) {
|
|
|
105
111
|
for (const operation of DSH_COMPANION_OPERATIONS) {
|
|
106
112
|
ctx.subagents.registerProvider(new KimiCompanionProvider(`kimi-${operation}`, operation, spec));
|
|
107
113
|
}
|
|
114
|
+
ctx.inject(["systemPrompt"], (promptCtx) => {
|
|
115
|
+
promptCtx.systemPrompt.section({
|
|
116
|
+
name: "kimi-plugin-cc:tool-boundaries",
|
|
117
|
+
order: promptCtx.systemPrompt.getSectionOrder("TOOL_SUBAGENT"),
|
|
118
|
+
text: () => DSH_TOOL_GUIDANCE,
|
|
119
|
+
});
|
|
120
|
+
});
|
|
108
121
|
ctx.inject(["commands"], (commandCtx) => {
|
|
109
122
|
const execute = async (invocation, argv) => {
|
|
110
123
|
const directories = commandCtx.get("workingDirectory");
|
|
@@ -595,7 +595,8 @@ function validateVersionSensitiveEvents(events, version) {
|
|
|
595
595
|
// Exact schemas reviewed at 2.0.0 (1b89e4b0), 2.0.1 (caf7d4e2),
|
|
596
596
|
// 2.0.2 (9d07f634), 2.1.0 (52437299), 2.1.1 (f67e6398), 2.2.0 (472192c3),
|
|
597
597
|
// 2.3.0 (b5e28fd6), 2.3.1 (684bae9e), 2.3.2 (b8dd3a13), 2.3.3 (ec495f0c),
|
|
598
|
-
// 2.4.0 (f26e4d91),
|
|
598
|
+
// 2.4.0 (f26e4d91), 2.4.1 (2c73f3f5), 2.5.0 (41c34143), and 2.5.1 (1b02030d):
|
|
599
|
+
// the additive events and HookDefSchema are unchanged.
|
|
599
600
|
// Require the full version string AND matching components; no inferred 2.x compatibility.
|
|
600
601
|
if (version.major === 2 && ((version.minor === 0 && ((version.version === "2.0.0" && version.patch === 0) ||
|
|
601
602
|
(version.version === "2.0.1" && version.patch === 1) ||
|
|
@@ -608,7 +609,9 @@ function validateVersionSensitiveEvents(events, version) {
|
|
|
608
609
|
(version.version === "2.3.2" && version.patch === 2) ||
|
|
609
610
|
(version.version === "2.3.3" && version.patch === 3))) ||
|
|
610
611
|
(version.minor === 4 && ((version.version === "2.4.0" && version.patch === 0) ||
|
|
611
|
-
(version.version === "2.4.1" && version.patch === 1)))
|
|
612
|
+
(version.version === "2.4.1" && version.patch === 1))) ||
|
|
613
|
+
(version.minor === 5 && ((version.version === "2.5.0" && version.patch === 0) ||
|
|
614
|
+
(version.version === "2.5.1" && version.patch === 1))))) {
|
|
612
615
|
return { valid: true };
|
|
613
616
|
}
|
|
614
617
|
if (version.major !== 0) {
|
|
@@ -45,10 +45,80 @@ const MATCHER_LINE_RE = /^matcher\s*=/;
|
|
|
45
45
|
/** `timeout = <int>` — the only other key our managed [[hooks]] table emits. */
|
|
46
46
|
const TIMEOUT_LINE_RE = /^timeout\s*=\s*\d+\s*$/;
|
|
47
47
|
const HOOKS_TABLE_RE = /^\[\[hooks\]\]\s*$/;
|
|
48
|
+
// Recognize comment lines lexically, not text inside TOML string values.
|
|
49
|
+
// Whole-document parsing remains the authority for TOML validity.
|
|
50
|
+
function scanTomlLines(contents) {
|
|
51
|
+
const markers = new Set();
|
|
52
|
+
const tables = new Set();
|
|
53
|
+
let state = "plain";
|
|
54
|
+
let line = 0;
|
|
55
|
+
let blankPrefix = true;
|
|
56
|
+
for (let i = 0; i < contents.length; i += 1) {
|
|
57
|
+
const c = contents[i];
|
|
58
|
+
if (c === "\n") {
|
|
59
|
+
line += 1;
|
|
60
|
+
blankPrefix = true;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const onlyWhitespaceBefore = blankPrefix;
|
|
64
|
+
if (c !== " " && c !== "\t" && c !== "\r")
|
|
65
|
+
blankPrefix = false;
|
|
66
|
+
if (state === "plain") {
|
|
67
|
+
if (c === "[" && onlyWhitespaceBefore)
|
|
68
|
+
tables.add(line);
|
|
69
|
+
if (c === "#") {
|
|
70
|
+
if (onlyWhitespaceBefore) {
|
|
71
|
+
const end = contents.indexOf("\n", i);
|
|
72
|
+
const comment = contents.slice(i, end < 0 ? contents.length : end).trim();
|
|
73
|
+
if (BEGIN_LINE_RE.test(comment) || END_LINE_RE.test(comment))
|
|
74
|
+
markers.add(line);
|
|
75
|
+
}
|
|
76
|
+
const end = contents.indexOf("\n", i);
|
|
77
|
+
if (end < 0)
|
|
78
|
+
break;
|
|
79
|
+
i = end - 1;
|
|
80
|
+
}
|
|
81
|
+
else if (c === '"' || c === "'") {
|
|
82
|
+
const triple = contents.slice(i, i + 3) === c.repeat(3);
|
|
83
|
+
state = c === '"' ? (triple ? "multibasic" : "basic") : (triple ? "multiliteral" : "literal");
|
|
84
|
+
if (triple)
|
|
85
|
+
i += 2;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
const basic = state === "basic" || state === "multibasic";
|
|
90
|
+
const quote = basic ? '"' : "'";
|
|
91
|
+
if (basic && c === "\\") {
|
|
92
|
+
if (contents[i + 1] === "\n") {
|
|
93
|
+
line += 1;
|
|
94
|
+
blankPrefix = true;
|
|
95
|
+
}
|
|
96
|
+
i += 1;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (c !== quote)
|
|
100
|
+
continue;
|
|
101
|
+
if (state === "basic" || state === "literal")
|
|
102
|
+
state = "plain";
|
|
103
|
+
else if (contents.slice(i, i + 3) === quote.repeat(3)) {
|
|
104
|
+
while (contents[i + 1] === quote)
|
|
105
|
+
i += 1;
|
|
106
|
+
state = "plain";
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return { markers, tables };
|
|
111
|
+
}
|
|
112
|
+
export function markerCommentLines(contents) {
|
|
113
|
+
return scanTomlLines(contents).markers;
|
|
114
|
+
}
|
|
48
115
|
/** Locate every BEGIN/END marker line, in order, with its parsed host id. */
|
|
49
116
|
function findMarkers(lines) {
|
|
50
117
|
const hits = [];
|
|
118
|
+
const comments = markerCommentLines(lines.join("\n"));
|
|
51
119
|
for (let i = 0; i < lines.length; i += 1) {
|
|
120
|
+
if (!comments.has(i))
|
|
121
|
+
continue;
|
|
52
122
|
const trimmed = lines[i].trim();
|
|
53
123
|
const begin = BEGIN_LINE_RE.exec(trimmed);
|
|
54
124
|
if (begin !== null) {
|
|
@@ -66,7 +136,7 @@ function findMarkers(lines) {
|
|
|
66
136
|
}
|
|
67
137
|
/**
|
|
68
138
|
* Pair BEGIN/END markers into well-formed blocks. Return the first orphan
|
|
69
|
-
* (BEGIN-without-END or END-without-BEGIN);
|
|
139
|
+
* (BEGIN-without-END or END-without-BEGIN); verification fails closed on either.
|
|
70
140
|
*/
|
|
71
141
|
function pairBlocks(lines) {
|
|
72
142
|
const markers = findMarkers(lines);
|
|
@@ -83,6 +153,8 @@ function pairBlocks(lines) {
|
|
|
83
153
|
// end
|
|
84
154
|
if (open === null)
|
|
85
155
|
return { orphan: "END-without-BEGIN" };
|
|
156
|
+
if (open.hostId !== marker.hostId)
|
|
157
|
+
return { orphan: "BEGIN-without-END" };
|
|
86
158
|
const body = validateBlockBody(lines.slice(open.index + 1, marker.index));
|
|
87
159
|
blocks.push({
|
|
88
160
|
hostId: open.hostId,
|
|
@@ -436,13 +508,13 @@ export function evaluateInstalled(contents, expectedCommand, opts) {
|
|
|
436
508
|
* The deletion range `[start, end)` is conservative: only the contiguous
|
|
437
509
|
* non-blank body after `[[hooks]]`, stopping at a blank line, table header, or
|
|
438
510
|
* managed marker so adjacent user content is preserved. A separate full-span
|
|
439
|
-
* scan
|
|
440
|
-
*
|
|
441
|
-
*
|
|
442
|
-
* caller's ownership checks.
|
|
511
|
+
* scan refuses any further TOML field before the next table, including quoted
|
|
512
|
+
* and dotted keys, so pruning cannot attach those fields to another table.
|
|
513
|
+
* `command` is decoded for the caller's ownership checks.
|
|
443
514
|
*/
|
|
444
515
|
export function findBareApprovalHookTables(contents) {
|
|
445
516
|
const lines = contents.split("\n").map((line) => line.replace(/\r$/, ""));
|
|
517
|
+
const tableLines = scanTomlLines(contents).tables;
|
|
446
518
|
const managed = new Set();
|
|
447
519
|
const paired = pairBlocks(lines);
|
|
448
520
|
if ("blocks" in paired) {
|
|
@@ -455,7 +527,7 @@ export function findBareApprovalHookTables(contents) {
|
|
|
455
527
|
for (let i = 0; i < lines.length; i += 1) {
|
|
456
528
|
if (managed.has(i))
|
|
457
529
|
continue;
|
|
458
|
-
if (!HOOKS_TABLE_RE.test(lines[i].trim()))
|
|
530
|
+
if (!tableLines.has(i) || !HOOKS_TABLE_RE.test(lines[i].trim()))
|
|
459
531
|
continue;
|
|
460
532
|
// Collect the contiguous table body.
|
|
461
533
|
let j = i + 1;
|
|
@@ -500,22 +572,16 @@ export function findBareApprovalHookTables(contents) {
|
|
|
500
572
|
}
|
|
501
573
|
j += 1;
|
|
502
574
|
}
|
|
503
|
-
//
|
|
504
|
-
//
|
|
505
|
-
//
|
|
506
|
-
|
|
507
|
-
// matcher is schema-valid but can disable the hook at runtime. Foreign keys
|
|
508
|
-
// and other TOML spellings are handled by whole-file schema validation and
|
|
509
|
-
// the parser-based installed checks, not by this prune heuristic.
|
|
510
|
-
let matcherBeyondBody = false;
|
|
575
|
+
// Blank lines and marker comments do not end a TOML table. Never leave any
|
|
576
|
+
// field behind to be rebound to a preceding table after pruning, regardless
|
|
577
|
+
// of the field's spelling or whether it is valid for a hook.
|
|
578
|
+
let fieldBeyondBody = false;
|
|
511
579
|
for (let k = j; k < lines.length; k += 1) {
|
|
512
580
|
const t = lines[k].trim();
|
|
513
|
-
|
|
514
|
-
// managed marker; a matcher past that point belongs to a different table.
|
|
515
|
-
if (t.startsWith("[") || BEGIN_LINE_RE.test(t) || END_LINE_RE.test(t))
|
|
581
|
+
if (tableLines.has(k))
|
|
516
582
|
break;
|
|
517
|
-
if (
|
|
518
|
-
|
|
583
|
+
if (t.length > 0 && !t.startsWith("#")) {
|
|
584
|
+
fieldBeyondBody = true;
|
|
519
585
|
break;
|
|
520
586
|
}
|
|
521
587
|
}
|
|
@@ -527,7 +593,7 @@ export function findBareApprovalHookTables(contents) {
|
|
|
527
593
|
commandDecoded.length > 0 &&
|
|
528
594
|
hasPreToolUseEvent &&
|
|
529
595
|
!hasMatcher &&
|
|
530
|
-
!
|
|
596
|
+
!fieldBeyondBody) {
|
|
531
597
|
ranges.push({ start: i, end: j, command: commandDecoded });
|
|
532
598
|
}
|
|
533
599
|
}
|
|
@@ -47,6 +47,7 @@ const RESERVED_PREFIX_FLAGS = new Set([
|
|
|
47
47
|
"--plan", "--auto", "-y", "--yolo", "--yes", "--auto-approve", "--manual",
|
|
48
48
|
// Model / profile / roots.
|
|
49
49
|
"-m", "--model", "--skills-dir", "--agent", "--agent-file", "--add-dir",
|
|
50
|
+
"--environment", "--environment-cwd",
|
|
50
51
|
// Terminal informational flags (would replace the run with a banner).
|
|
51
52
|
"-V", "--version", "-h", "--help",
|
|
52
53
|
]);
|
|
@@ -45,6 +45,8 @@ export const NATIVE_V2_CERTIFIED_VERSIONS = Object.freeze([
|
|
|
45
45
|
"2.3.3",
|
|
46
46
|
"2.4.0",
|
|
47
47
|
"2.4.1",
|
|
48
|
+
"2.5.0",
|
|
49
|
+
"2.5.1",
|
|
48
50
|
]);
|
|
49
51
|
/** Per-operation native-v2 certification. One green operation never certifies another. */
|
|
50
52
|
export const NATIVE_V2_CERTIFIED = new Map([
|
|
@@ -127,6 +129,7 @@ export async function prepareKimiExecutionPlan(options) {
|
|
|
127
129
|
kimiHome: resolveKimiHome(options.env, options.cwd),
|
|
128
130
|
env: options.env,
|
|
129
131
|
resumeSessionId: options.resumeSessionId,
|
|
132
|
+
expectedExecutionRoot: options.cwd,
|
|
130
133
|
stage: "kimi-engine.plan",
|
|
131
134
|
});
|
|
132
135
|
};
|