kimi-plugin-cc 2.4.5 → 2.5.2

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.
Files changed (114) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +22 -0
  4. package/README.md +103 -29
  5. package/dist/commands/setup.js +78 -51
  6. package/dist/dsh/companion.js +177 -0
  7. package/dist/dsh/index.js +169 -0
  8. package/dist/hooks/install-paths.js +2 -2
  9. package/dist/hooks/managed-block.js +86 -20
  10. package/dist/version.js +1 -1
  11. package/docs/migration.md +4 -2
  12. package/docs/models.md +5 -1
  13. package/docs/registry-releases.md +84 -20
  14. package/docs/safety.md +25 -10
  15. package/docs/upstream-2.4.1-certification.md +6 -0
  16. package/package.json +75 -8
  17. package/plugins/kimi-codex/.codex-plugin/plugin.json +1 -1
  18. package/plugins/kimi-codex/dist/commands/setup.js +78 -51
  19. package/plugins/kimi-codex/dist/dsh/companion.js +177 -0
  20. package/plugins/kimi-codex/dist/dsh/index.js +169 -0
  21. package/plugins/kimi-codex/dist/hooks/install-paths.js +2 -2
  22. package/plugins/kimi-codex/dist/hooks/managed-block.js +86 -20
  23. package/plugins/kimi-codex/dist/version.js +1 -1
  24. package/plugins/kimi-dsh/LICENSE +201 -0
  25. package/plugins/kimi-dsh/README.md +83 -0
  26. package/plugins/kimi-dsh/cordis.patch.yml +45 -0
  27. package/plugins/kimi-dsh/dist/background-spawn.js +216 -0
  28. package/plugins/kimi-dsh/dist/cli-cancellation.js +43 -0
  29. package/plugins/kimi-dsh/dist/cli-client.js +876 -0
  30. package/plugins/kimi-dsh/dist/commands/ask.js +336 -0
  31. package/plugins/kimi-dsh/dist/commands/cancel.js +216 -0
  32. package/plugins/kimi-dsh/dist/commands/cli-helpers.js +201 -0
  33. package/plugins/kimi-dsh/dist/commands/models.js +140 -0
  34. package/plugins/kimi-dsh/dist/commands/not-implemented.js +4 -0
  35. package/plugins/kimi-dsh/dist/commands/pursue.js +307 -0
  36. package/plugins/kimi-dsh/dist/commands/recover-swarm.js +135 -0
  37. package/plugins/kimi-dsh/dist/commands/registry.js +74 -0
  38. package/plugins/kimi-dsh/dist/commands/repair-sessions.js +160 -0
  39. package/plugins/kimi-dsh/dist/commands/replay.js +124 -0
  40. package/plugins/kimi-dsh/dist/commands/rescue.js +347 -0
  41. package/plugins/kimi-dsh/dist/commands/result.js +66 -0
  42. package/plugins/kimi-dsh/dist/commands/review-gate.js +390 -0
  43. package/plugins/kimi-dsh/dist/commands/review.js +220 -0
  44. package/plugins/kimi-dsh/dist/commands/setup.js +1074 -0
  45. package/plugins/kimi-dsh/dist/commands/status.js +27 -0
  46. package/plugins/kimi-dsh/dist/commands/swarm.js +754 -0
  47. package/plugins/kimi-dsh/dist/commands/worker.js +25 -0
  48. package/plugins/kimi-dsh/dist/companion.js +97 -0
  49. package/plugins/kimi-dsh/dist/config.js +22 -0
  50. package/plugins/kimi-dsh/dist/dsh/companion.js +177 -0
  51. package/plugins/kimi-dsh/dist/dsh/index.js +169 -0
  52. package/plugins/kimi-dsh/dist/errors.js +40 -0
  53. package/plugins/kimi-dsh/dist/git.js +224 -0
  54. package/plugins/kimi-dsh/dist/help.js +43 -0
  55. package/plugins/kimi-dsh/dist/hook-escape.js +71 -0
  56. package/plugins/kimi-dsh/dist/hooks/approval-hook.js +101 -0
  57. package/plugins/kimi-dsh/dist/hooks/approval-policy.js +211 -0
  58. package/plugins/kimi-dsh/dist/hooks/config-safety.js +653 -0
  59. package/plugins/kimi-dsh/dist/hooks/install-paths.js +428 -0
  60. package/plugins/kimi-dsh/dist/hooks/install.js +210 -0
  61. package/plugins/kimi-dsh/dist/hooks/managed-block.js +685 -0
  62. package/plugins/kimi-dsh/dist/hooks/review-gate-stop.js +19 -0
  63. package/plugins/kimi-dsh/dist/job-id.js +12 -0
  64. package/plugins/kimi-dsh/dist/job-store.js +659 -0
  65. package/plugins/kimi-dsh/dist/jobs.js +217 -0
  66. package/plugins/kimi-dsh/dist/kimi-command.js +70 -0
  67. package/plugins/kimi-dsh/dist/kimi-engine.js +632 -0
  68. package/plugins/kimi-dsh/dist/kimi-errors.js +260 -0
  69. package/plugins/kimi-dsh/dist/kimi-home.js +9 -0
  70. package/plugins/kimi-dsh/dist/kimi-timeouts.js +52 -0
  71. package/plugins/kimi-dsh/dist/kimi-version-probe.js +216 -0
  72. package/plugins/kimi-dsh/dist/logging.js +22 -0
  73. package/plugins/kimi-dsh/dist/mcp/binding.js +58 -0
  74. package/plugins/kimi-dsh/dist/mcp/job-control.js +345 -0
  75. package/plugins/kimi-dsh/dist/mcp/launcher.js +257 -0
  76. package/plugins/kimi-dsh/dist/mcp/operations.js +301 -0
  77. package/plugins/kimi-dsh/dist/mcp/protocol.js +275 -0
  78. package/plugins/kimi-dsh/dist/mcp/server.js +8 -0
  79. package/plugins/kimi-dsh/dist/mcp/stdio.js +78 -0
  80. package/plugins/kimi-dsh/dist/mcp/supervised-child.js +15 -0
  81. package/plugins/kimi-dsh/dist/mcp/tool-result.js +7 -0
  82. package/plugins/kimi-dsh/dist/mcp/workspace.js +34 -0
  83. package/plugins/kimi-dsh/dist/model-command.js +32 -0
  84. package/plugins/kimi-dsh/dist/native-v2-preflight.js +417 -0
  85. package/plugins/kimi-dsh/dist/package.json +1 -0
  86. package/plugins/kimi-dsh/dist/parsing.js +620 -0
  87. package/plugins/kimi-dsh/dist/paths.js +27 -0
  88. package/plugins/kimi-dsh/dist/plugin-data.js +98 -0
  89. package/plugins/kimi-dsh/dist/process-tree.js +479 -0
  90. package/plugins/kimi-dsh/dist/prompt-size.js +18 -0
  91. package/plugins/kimi-dsh/dist/render.js +177 -0
  92. package/plugins/kimi-dsh/dist/rescue-approval.js +865 -0
  93. package/plugins/kimi-dsh/dist/schemas/review-gate-output.js +43 -0
  94. package/plugins/kimi-dsh/dist/session-recovery.js +200 -0
  95. package/plugins/kimi-dsh/dist/session-title.js +293 -0
  96. package/plugins/kimi-dsh/dist/stream-json.js +346 -0
  97. package/plugins/kimi-dsh/dist/swarm-activation.js +834 -0
  98. package/plugins/kimi-dsh/dist/swarm-recovery.js +134 -0
  99. package/plugins/kimi-dsh/dist/types.js +1 -0
  100. package/plugins/kimi-dsh/dist/vendor/shell-quote/LICENSE +24 -0
  101. package/plugins/kimi-dsh/dist/vendor/shell-quote/index.js +1 -0
  102. package/plugins/kimi-dsh/dist/vendor/shell-quote/parse.js +229 -0
  103. package/plugins/kimi-dsh/dist/vendor/smol-toml/LICENSE +24 -0
  104. package/plugins/kimi-dsh/dist/vendor/smol-toml/date.js +156 -0
  105. package/plugins/kimi-dsh/dist/vendor/smol-toml/error.js +67 -0
  106. package/plugins/kimi-dsh/dist/vendor/smol-toml/extract.js +293 -0
  107. package/plugins/kimi-dsh/dist/vendor/smol-toml/parse.js +164 -0
  108. package/plugins/kimi-dsh/dist/vendor/smol-toml/primitive.js +151 -0
  109. package/plugins/kimi-dsh/dist/vendor/smol-toml/struct.js +156 -0
  110. package/plugins/kimi-dsh/dist/vendor/smol-toml/util.js +57 -0
  111. package/plugins/kimi-dsh/dist/version.js +7 -0
  112. package/plugins/kimi-dsh/package.json +78 -0
  113. package/plugins/kimi-dsh/scripts/companion.sh +37 -0
  114. package/plugins/kimi-dsh/scripts/review-gate-hook.sh +32 -0
@@ -1,16 +1,18 @@
1
1
  # Registry releases
2
2
 
3
3
  The npm package `kimi-plugin-cc` ships the Claude plugin at its root and the
4
- self-contained Codex plugin under `plugins/kimi-codex`. GitHub marketplaces
5
- remain available. The package has no install scripts and no separate CLI.
4
+ self-contained Codex and DSH plugins under `plugins/`. DSH users install that
5
+ same root package with `dsh plugin --profile <name> add kimi-plugin-cc`.
6
+ GitHub marketplaces remain available. The package has no install scripts and
7
+ no separate CLI.
6
8
 
7
9
  ## Publishing status
8
10
 
9
- Plugin **2.4.5** adds exact CLI **2.4.1** certification for all eight operations,
10
- with the existing host-preactivated swarm lifecycle and durable recovery.
11
- See the [certification record](./upstream-2.4.1-certification.md).
12
- Release execution and registry verification are recorded in the release notes;
13
- publication uses the same validated-tarball OIDC workflow below.
11
+ 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.
12
+
13
+ Plugin **2.5.1** adds DeepSeek Harness through the existing root npm package and its established OIDC workflow. [Release v2.5.1](https://github.com/linxule/kimi-plugin-cc/releases/tag/v2.5.1) is anchored at `afe68ee2f0e7744e1fc97eb0f1e4007585d969d0`; its [CI check](https://github.com/linxule/kimi-plugin-cc/actions/runs/41335810767) and [release-event publishing run](https://github.com/linxule/kimi-plugin-cc/actions/runs/41336273042) passed. The registry tarball has SHA-1 `f0e42febcb14082be6a654b9b0830885242fd92a`, contains 328 files, matches the CI artifact, and carries signed provenance (sigstore log index `3635166158`). A fresh registry installation into an isolated DSH profile composed all seven providers and booted the Web host. DSH users install with `dsh plugin --profile <name> add kimi-plugin-cc`; no second npm package or Trusted Publishing binding is required.
14
+
15
+ Plugin **2.4.5** certifies exact CLI **2.4.1** for all eight operations with host-preactivated swarm sessions. [Release v2.4.5](https://github.com/linxule/kimi-plugin-cc/releases/tag/v2.4.5) is anchored at `de8c195c2986443a088b674717133b164d2e4ef2`; its [CI check](https://github.com/linxule/kimi-plugin-cc/actions/runs/41226096281) and [release-event publishing run](https://github.com/linxule/kimi-plugin-cc/actions/runs/41226438326) passed. The registry tarball has SHA-1 `39f97a6de4b26ad51ac77305054682fcd5cd0164`, contains 233 files, and matches the CI artifact byte-for-byte. Fresh registry installs passed setup/check and unsupported-version refusal for both Claude Code and Codex, and both compiled mirrors are byte-identical to the release checkout. See the [certification record](./upstream-2.4.1-certification.md) and structured verification at `.claude/kimi-code-research/release-2.4.5/registry-verification.json`.
14
16
 
15
17
  Plugin **2.4.2** adds durable interrupted-swarm recovery and Windows spawn diagnostics.
16
18
  Exact CLI certification remains unchanged through **2.3.2**; this is not a new
@@ -80,9 +82,11 @@ file at the tag, so fixes to `publish.yml` reach an existing tag only through
80
82
  ## Validation
81
83
 
82
84
  Use the repository's normal version bump and generated-surface procedure in
83
- AGENTS.md. Keep plugin and npm versions identical. Certification for new Kimi
84
- Code versions still requires the source audit and real-binary controls; adding
85
- a registry channel does not certify another CLI version.
85
+ AGENTS.md. Keep plugin and npm versions identical, and update
86
+ `DSH_PUBLISH_TIMESTAMP` with the actual publication time so the generated DSH
87
+ Discover index stays current. Certification for new Kimi Code versions still
88
+ requires the source audit and real-binary controls; adding a registry channel
89
+ does not certify another CLI version.
86
90
 
87
91
  ```sh
88
92
  bun install --frozen-lockfile
@@ -90,15 +94,73 @@ bun run build
90
94
  bun run generate:surfaces
91
95
  # Review and stage generated distributions before the drift gate.
92
96
  bun run check
93
- bun run check:package .tmp/npm
97
+ bun run check:consumer-security
98
+ node scripts/check-package.mjs .tmp/npm
99
+ node scripts/check-dsh-package.mjs .tmp/dsh-npm
94
100
  ```
95
101
 
96
- The package check inspects the actual tarball, rejects sync-conflict copies in
102
+ The root package check inspects the actual tarball, rejects sync-conflict copies in
97
103
  any path component, verifies runtime copies and vendored licenses, and
98
- installs/checks the safety hook in isolated homes for both hosts. It also
104
+ installs/checks the safety hook in isolated homes for all three hosts. It also
99
105
  exercises unsupported-version refusal using a fake CLI and modern MCP discovery
100
- plus legacy initialization on both extracted compiled distributions. It does
101
- not invoke a model or read the maintainer's Kimi configuration.
106
+ plus legacy initialization on the extracted compiled distributions. The DSH
107
+ check installs that same root tarball into an isolated DSH home, verifies all
108
+ seven tools, and boots the Web host. Neither check invokes a model or reads the
109
+ maintainer's Kimi configuration.
110
+
111
+ The consumer-security gate installs the packed package into a fresh npm project
112
+ and audits the resolved production dependency graph. DSH services and its tool
113
+ adapter are optional peers supplied by the DSH host; ordinary npm installation
114
+ for Claude Code or Codex does not install the harness. Optional peers still
115
+ carry compatibility ranges when present. The gate runs in CI and release
116
+ validation and refuses high/critical advisories, failed audits, and incomplete
117
+ reports. The checkout's overrides do not constrain downstream npm resolution.
118
+
119
+ ### Patched DSH host and profiles
120
+
121
+ DSH 0.2.1-rc.5 and 0.2.2-alpha.2 still pin vulnerable `zod` and `undici`
122
+ versions. See the [upstream dependency report](https://github.com/deepseek-ai/deepseek-harness/discussions/10847).
123
+ Use a patched DSH host installation with `zod >=4.6.6` and `undici >=8.12.2`.
124
+ This is an explicit installation requirement; the plugin does not rewrite the
125
+ host or profiles automatically. A profile override alone does not patch copies
126
+ inside a separately installed DSH CLI.
127
+
128
+ For example, create a dedicated npm host project (Node >=24.9), then use its
129
+ DSH binary for plugin installation and launch:
130
+
131
+ ```sh
132
+ mkdir -p ~/.local/share/kimi-dsh-host
133
+ cd ~/.local/share/kimi-dsh-host
134
+ npm init -y
135
+ npm pkg set 'overrides.zod=^4.6.6' 'overrides.undici=^8.12.2'
136
+ npm install @deepseek-ai/dsh@0.2.1-rc.5
137
+ npm audit --omit=dev --audit-level=high
138
+ ./node_modules/.bin/dsh plugin --profile web add kimi-plugin-cc
139
+ ./node_modules/.bin/dsh plugin --profile web audit --prod --audit-level high
140
+ ./node_modules/.bin/dsh web --no-open
141
+ ```
142
+
143
+ The host project is separate from `$DSH_HOME/profiles/web`. Keep using that
144
+ project's DSH binary so the audited host is the one that runs. The commands
145
+ above install the published plugin; use a validated local tarball when testing
146
+ an unpublished change.
147
+
148
+ If an existing profile separately installs DSH packages, its owner must also
149
+ patch and audit that profile:
150
+
151
+ ```sh
152
+ dsh plugin --profile web pkg set 'pnpm.overrides.zod=^4.6.6' 'pnpm.overrides.undici=^8.12.2'
153
+ dsh plugin --profile web install
154
+ dsh plugin --profile web audit --prod --audit-level high
155
+ ```
156
+
157
+ Replace `web` with the intended profile, and run these commands with the patched
158
+ host binary. Overrides affect dependency resolution and require application
159
+ checks. The DSH package gate creates a fresh host with the declared overrides,
160
+ audits both that host and the installed profile for high/critical advisories,
161
+ then verifies all seven tool compositions and Web boot using that same host.
162
+ These checks do not claim that every moderate/low advisory is fixed or that
163
+ upstream has published a patched release.
102
164
 
103
165
  ## First publication (completed bootstrap reference)
104
166
 
@@ -135,8 +197,10 @@ operations; Bun remains the development toolchain.
135
197
 
136
198
  Push the reviewed release commit, wait for CI, tag it, and publish its GitHub
137
199
  release. `.github/workflows/publish.yml` runs on `release.published`; it checks
138
- the tag against `package.json`, runs the full gate and package smoke, then
139
- publishes the validated tarball through OIDC with provenance. The publish job
140
- has no source checkout or build scripts. A manual dispatch with an existing
141
- release tag is available for recovery after a verified publication failure.
142
- Check npm before retrying; released versions are immutable.
200
+ the tag against `package.json`, runs the full gate and both package smokes, then
201
+ publishes the root tarball through OIDC with provenance. The DSH smoke installs
202
+ that same tarball directly into an isolated DSH profile, so no second npm
203
+ package or Trusted Publishing binding is needed. The publish job has no source
204
+ checkout or build scripts. A manual dispatch with an existing release tag is
205
+ available for recovery after a verified publication failure. Check npm before
206
+ retrying; released versions are immutable.
package/docs/safety.md CHANGED
@@ -72,26 +72,29 @@ Key constraints (verified by [`runtime/hooks/managed-block.ts`](../runtime/hooks
72
72
  1. **No `matcher` line.** kimi-code compiles the matcher field via `new RegExp(matcher)`. An empty matcher means "fire for every tool." `new RegExp("*")` throws, which kimi-code catches and treats as "no matcher" → silently disables the hook. The installer omits the field intentionally; the verifier rejects any block that contains a `matcher = ...` line.
73
73
  2. **Absolute Node path.** kimi-code spawns hook commands via `/bin/sh -c "<command>"`. If we wrote bare `node`, the shell would resolve it against kimi-code's PATH at execution time. On a system where kimi-code launches from a GUI/LaunchAgent with a sanitized PATH (nvm/asdf/mise users), bare `node` would exit 127 → hook protocol treats non-{0,2} as **allow** → safety contract collapses with no user-visible signal.
74
74
  3. **TOML basic-string escaping.** The command field is a TOML basic string. Quotes, backslashes, newlines, and control characters in the resolved hook path are escaped during install; paths containing characters that cannot be safely escaped (literal `"`, raw control chars) are rejected up front with `SETUP_HOOK_PATH_UNSAFE`.
75
- 4. **Exact marker grammar.** The verifier rejects stray comments containing the marker tag, partial references, duplicate blocks (from a setup race), and orphan markers (from a manual edit). The same parser is used by both the installer and the per-call verifier so the two cannot disagree.
75
+ 4. **Exact marker grammar.** The verifier rejects duplicate blocks and orphan or mismatched host markers. Install removes lexically proven unpaired marker comments across hosts, preserving surrounding content and all foreign hook tables, and warns. Text inside TOML strings is not a marker. The verifier remains fail-closed until repair and full validation succeed.
76
76
  5. **Exact command equality, per host.** The verifier reconstructs the canonical command byte string from the current env and equality-checks the installed `command` (never a substring — a crafted `true # /path/to/approval-hook.js` runs only `true` and would auto-allow). This holds *per host id* (below): each host verifies its own block.
77
77
  6. **Full-config parse and whole-hook-array validation.** The verifier parses the complete file with vendored `smol-toml@1.9.1` with upstream fixes included and no local JavaScript modifications (see `runtime/vendor/smol-toml/README.md`), before inspecting the managed block. It then applies upstream's strict config schema to every configured hook (`event`, optional string `matcher`, non-empty `command`, optional integer `timeout` 1–600, no extra fields). A malformed unrelated TOML value or one invalid foreign hook can make kimi-code discard enforcement; both therefore fail verification.
78
78
  7. **Serialized, identity-bound setup mutations.** Install and uninstall hold a private `0o600` sibling lock across the complete read-modify-write transaction. The candidate lock is published atomically; lock reads are no-follow, nonblocking, regular-file-only, and capped at 4 KiB. Acquisition is bounded, live owners are never stolen, stale recovery is guarded by a nonrecursive recovery lease, and inode/token ownership is rechecked before config mutation or lock removal. Simultaneous Claude Code/Codex setup runs therefore cannot lose either host's block, and a replaced lock is never treated as ours. A newly created Kimi home is `0o700`; atomic config writes remain `0o600`.
79
79
  8. **Inline hook arrays are normalized before insertion.** kimi-code accepts both `hooks = [...]` and `[[hooks]]`, but appending a managed array-of-tables block after an inline assignment can produce a conflicting document. Setup converts a valid inline hook array to canonical `[[hooks]]` tables before inserting the host block and emits an explicit warning that formatting and comments inside that assignment are normalized. Surrounding config bytes are preserved.
80
80
 
81
- ### Host scoping (Claude Code ↔ Codex share one config)
81
+ ### Host scoping (all hosts share one config)
82
82
 
83
83
  Claude Code and Codex install this plugin to **different, version-stamped
84
- paths** (`~/.claude/plugins/cache/…` vs `~/.codex/plugins/cache/…`) but read
85
- the **same** `~/.kimi-code/config.toml`. So the managed block is **host-scoped**:
84
+ paths** (`~/.claude/plugins/cache/…` vs `~/.codex/plugins/cache/…`), while DSH
85
+ uses its own profile package path. All read the **same**
86
+ `~/.kimi-code/config.toml`. So the managed block is **host-scoped**:
86
87
  its marker carries a `:<host-id>` suffix, and each host owns, verifies, and
87
88
  uninstalls only its **own** block.
88
89
 
89
90
  - **Host id** (`runtime/hooks/install-paths.ts::resolveHostId`) is derived
90
91
  version-independently from the hook path: `~/.claude/…` → `claude-code`,
91
- `~/.codex/…` → `codex`, else a stable `host-<sha1>` for dev checkouts. A
92
- plugin upgrade *refreshes* the same host's block rather than adding a new one.
93
- - **No clobbering.** `/kimi:setup` (Claude) and `$kimi-setup` (Codex) each write
94
- their own block and leave the other's byte-identical. Before v1.7.0 there was
92
+ `~/.codex/…` → `codex`, else a stable `host-<sha1>` for dev checkouts. DSH
93
+ explicitly pins `dsh`. A plugin upgrade *refreshes* the same host's block
94
+ rather than adding a new one.
95
+ - **No clobbering.** `/kimi:setup` (Claude), `$kimi-setup` (Codex), and
96
+ `/kimi-setup` (DSH) each write their own block and leave the others
97
+ byte-identical. Before v1.7.0 there was
95
98
  a single block exact-matched to the running host, so the two hosts overwrote
96
99
  each other and the loser's write-commands refused with "hook path drift."
97
100
  - **Enforcement is redundant-safe.** kimi-code fires *every* `[[hooks]]` entry
@@ -103,8 +106,8 @@ uninstalls only its **own** block.
103
106
  - **Migration + cleanup.** A pre-v1.7.0 un-suffixed block is adopted and
104
107
  converted in place by the current host on its next `/kimi:setup`. Orphaned,
105
108
  marker-less `[[hooks]]` entries that are unambiguously our approval hook
106
- (canonical command, `approval-hook.js` under a kimi-marketplace/kimi-plugin-cc
107
- tree) are pruned **host-scoped** (v1.8.2): install and scoped uninstall touch
109
+ (canonical command, `approval-hook.js` under a kimi-marketplace/kimi-plugin-cc/
110
+ dsh-kimi-plugin-cc tree) are pruned **host-scoped** (v1.8.2): install and scoped uninstall touch
108
111
  only tables whose command path derives to the *current* host. `/kimi:setup
109
112
  --uninstall` removes only the current host's block (plus legacy + this host's
110
113
  orphans); `/kimi:setup --uninstall --all` removes every host's block and every
@@ -123,6 +126,18 @@ uninstalls only its **own** block.
123
126
  hook and is left byte-untouched (pre-v1.8.2, the host-blind prune here caused
124
127
  each host's setup to disarm the other after any kimi login — the "seesaw").
125
128
 
129
+ ## DeepSeek Harness providers
130
+
131
+ DSH's host and any profile-local harness packages must use patched `zod >=4.6.6` and `undici >=8.12.2`. The plugin treats DSH services as optional host-provided peers; it does not install the harness for other hosts or rewrite an existing DSH installation. Audit the actual host installation as well as the profile. See [patched DSH setup](./registry-releases.md#patched-dsh-host-and-profiles).
132
+
133
+ The DSH bundle exposes the same seven operations as the other hosts: `kimi_ask`, `kimi_review`, `kimi_challenge`, `kimi_rescue`, `kimi_pursue`, `kimi_swarm`, and `kimi_swarm_write`. Each activation starts the existing companion through DSH's managed subprocess service and uses the same `kimi -p` transport, hook verification, exact-version certification, job store, and response or safety budgets. ACP is not used.
134
+
135
+ Run `/kimi-setup` explicitly before delegating. Installing or enabling the bundle does not modify Kimi configuration. DSH uses host id `dsh` and `$DSH_HOME/plugin-data/kimi-plugin-cc` unless an absolute data-root override selects another store. Its provider-owned environment clears other-host roots/data, hook/script overrides, trusted roots, and verification bypasses instead of inheriting them.
136
+
137
+ The parent receives only complete successful stdout. stderr remains on the host, and nonzero, empty, or truncated output becomes a bounded diagnostic rather than partial prose. Cancellation terminates the managed process range and waits for quiescence. DSH activation ids are distinct from companion job ids; use `/kimi-status`, `/kimi-result`, or `/kimi-cancel` for the persisted job.
138
+
139
+ Write boundaries are identical to the companion commands: rescue and pursue edit the real selected workspace, ask/review/challenge and read-only swarm deny writes and shell commands, and swarm-write works from born HEAD in a temporary worktree and returns a patch without applying or committing it. `/kimi-setup --uninstall` removes only the DSH managed block; the all-host uninstall flag is not exposed.
140
+
126
141
  ## The hook's per-command policy
127
142
 
128
143
  [`runtime/hooks/approval-policy.ts`](../runtime/hooks/approval-policy.ts) is the single source of truth:
@@ -43,6 +43,12 @@ Controls used a task-authorized dedicated Agent Key in process memory/environmen
43
43
  - Codex's targeted re-review requested genuine error results, legacy-compatible turn starts, and a whole-workspace settlement assertion. A subsequent review added marker-prefix, call-derived turn-ID, and directory-entry requirements. The final production matrix (`production-prefix-smoke.log`) passed **24 tests and 167 assertions with no skips in 440.66 seconds**. All seven call-derived turn IDs had same-call Write denials with `isError: true` and the managed-message prefix, and the entire workspace was empty at settlement.
44
44
  - Literal standard `bun run check` (`check-standard-final.log`): **1,318 pass, 30 expected opt-in skips, 0 fail, 17,319 assertions across 79 files**, 358.83 seconds. Earlier parallel attempts hit timing-sensitive lifecycle failures; the focused suites and a complete serial run passed before the literal standard gate also passed. Codex's final targeted re-review returned **APPROVE**, closing all original and subsequent findings. Final edited-byte package validation (`package-standard-final.log`) passed **233 files** and includes this record.
45
45
 
46
+ ## Release and publication
47
+
48
+ Plugin 2.4.5 was committed as `de8c195c2986443a088b674717133b164d2e4ef2` and pushed to `main`. [Release-commit CI 41226096281](https://github.com/linxule/kimi-plugin-cc/actions/runs/41226096281) passed the full check/package and Windows spawn jobs. Annotated tag `v2.4.5` and [GitHub release](https://github.com/linxule/kimi-plugin-cc/releases/tag/v2.4.5) were created. [Publish workflow 41226438326](https://github.com/linxule/kimi-plugin-cc/actions/runs/41226438326) passed its build and npm publish jobs.
49
+
50
+ The registry tarball has SHA-1 `39f97a6de4b26ad51ac77305054682fcd5cd0164`, contains 233 files, and matches the CI artifact byte-for-byte. Fresh npm registry installations passed isolated setup/check and unsupported-version refusal for both hosts, and the compiled mirrors are byte-identical to the release checkout. Structured proof is retained at `.claude/kimi-code-research/release-2.4.5/registry-verification.json`. Installed hosts and operator CLI pins remain unchanged.
51
+
46
52
  ## Preserved boundaries
47
53
 
48
54
  Certification remains exact and per-operation. Legacy-v1 ends at 0.41.x. Plan mode and unsafe experimental selectors remain refused. Hook verification, trusted-root confinement, concurrency/defaults, one print runner, memory-only web tokens, finite budgets, process/session identity checks, deactivation, and recovery ownership are unchanged. Existing Git trust and hook-process failure residuals remain documented; this release does not make the hook an OS sandbox. Publication does not install or re-pin either host.
package/package.json CHANGED
@@ -1,30 +1,94 @@
1
1
  {
2
2
  "name": "kimi-plugin-cc",
3
- "version": "2.4.5",
4
- "description": "Kimi for Claude Code and Codex: code questions, independent reviews, bounded fixes, autonomous goals, parallel tasks, and MCP tools through the local Kimi Code CLI.",
3
+ "version": "2.5.2",
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
+ "main": "dist/dsh/index.js",
6
7
  "engines": {
7
- "node": ">=22.5.0"
8
+ "node": ">=22.5.0",
9
+ "dsh": ">=0.2.1-rc.5 <0.3.0"
10
+ },
11
+ "dsh": {
12
+ "bundle": {
13
+ "patch": "./plugins/kimi-dsh/cordis.patch.yml"
14
+ }
8
15
  },
9
16
  "scripts": {
10
- "check": "bun run build && bun run check:contracts && bun run check:surfaces && bunx tsc --noEmit && bun test && git diff --exit-code -- dist plugins/kimi-codex && UNTRACKED=\"$(git ls-files --others --exclude-standard -- dist plugins/kimi-codex)\" && { [ -z \"$UNTRACKED\" ] || { echo \"drift gate: untracked dist/codex files (stage them and re-run check):\"; echo \"$UNTRACKED\"; exit 1; }; }",
17
+ "check": "bun run build && bun run check:contracts && bun run check:surfaces && bunx tsc --noEmit && bun test && git diff --exit-code -- dist plugins/kimi-codex plugins/kimi-dsh && UNTRACKED=\"$(git ls-files --others --exclude-standard -- dist plugins/kimi-codex plugins/kimi-dsh)\" && { [ -z \"$UNTRACKED\" ] || { echo \"drift gate: untracked dist/codex/dsh files (stage them and re-run check):\"; echo \"$UNTRACKED\"; exit 1; }; }",
11
18
  "check:surfaces": "bun run scripts/generate-surfaces.ts --check",
12
19
  "generate:surfaces": "bun run scripts/generate-surfaces.ts",
13
20
  "build": "rm -rf dist && bunx tsc -p tsconfig.build.json && mkdir -p dist/vendor/shell-quote dist/vendor/smol-toml && cp runtime/vendor/shell-quote/LICENSE dist/vendor/shell-quote/LICENSE && cp runtime/vendor/smol-toml/*.js runtime/vendor/smol-toml/LICENSE dist/vendor/smol-toml/ && echo '{\"type\":\"module\"}' > dist/package.json",
14
21
  "companion": "node --import tsx runtime/companion.ts",
15
22
  "test": "bun test",
16
23
  "smoke:real": "KIMI_PLUGIN_CC_SMOKE=1 bun test tests/runtime/real-binary-smoke.test.ts --path-ignore-patterns '**/worktrees/**'",
17
- "check:package": "node scripts/check-package.mjs",
24
+ "check:package": "node scripts/check-package.mjs && node scripts/check-dsh-package.mjs",
25
+ "check:consumer-security": "node scripts/check-consumer-security.mjs",
18
26
  "check:contracts": "node scripts/check-contract-size.mjs"
19
27
  },
20
28
  "devDependencies": {
21
- "@types/bun": "^1.4.2",
22
- "@types/node": "^26.5.1",
29
+ "@deepseek-ai/cordis": "4.0.5",
30
+ "@deepseek-ai/dsh-commands": "0.2.1-rc.5",
31
+ "@deepseek-ai/dsh-llm": "0.2.1-rc.5",
32
+ "@deepseek-ai/dsh-session": "0.2.1-rc.5",
33
+ "@deepseek-ai/dsh-subagent": "0.2.1-rc.5",
34
+ "@deepseek-ai/dsh-subprocess": "0.2.1-rc.5",
35
+ "@deepseek-ai/dsh-system-prompt": "0.2.1-rc.5",
36
+ "@deepseek-ai/dsh-tool-subagent": "0.2.1-rc.5",
37
+ "@deepseek-ai/dsh-working-directory": "0.2.1-rc.5",
38
+ "@types/bun": "^1.4.4",
39
+ "@types/node": "^26.6.6",
23
40
  "smol-toml": "1.9.1",
24
41
  "tsx": "^4.23.13",
25
42
  "typescript": "^7.0.2"
26
43
  },
27
- "dependencies": {},
44
+ "dependencies": {
45
+ "@deepseek-ai/dsh-brand": "0.2.1-rc.5",
46
+ "@deepseek-ai/schemastery": "3.18.5"
47
+ },
48
+ "peerDependencies": {
49
+ "@deepseek-ai/cordis": "~4.0.5",
50
+ "@deepseek-ai/dsh-commands": ">=0.2.1-rc.5 <0.3.0",
51
+ "@deepseek-ai/dsh-llm": ">=0.2.1-rc.5 <0.3.0",
52
+ "@deepseek-ai/dsh-session": ">=0.2.1-rc.5 <0.3.0",
53
+ "@deepseek-ai/dsh-subagent": ">=0.2.1-rc.5 <0.3.0",
54
+ "@deepseek-ai/dsh-subprocess": ">=0.2.1-rc.5 <0.3.0",
55
+ "@deepseek-ai/dsh-system-prompt": ">=0.2.1-rc.5 <0.3.0",
56
+ "@deepseek-ai/dsh-working-directory": ">=0.2.1-rc.5 <0.3.0",
57
+ "@deepseek-ai/dsh-tool-subagent": ">=0.2.1-rc.5 <0.3.0"
58
+ },
59
+ "peerDependenciesMeta": {
60
+ "@deepseek-ai/cordis": {
61
+ "optional": true
62
+ },
63
+ "@deepseek-ai/dsh-commands": {
64
+ "optional": true
65
+ },
66
+ "@deepseek-ai/dsh-llm": {
67
+ "optional": true
68
+ },
69
+ "@deepseek-ai/dsh-session": {
70
+ "optional": true
71
+ },
72
+ "@deepseek-ai/dsh-subagent": {
73
+ "optional": true
74
+ },
75
+ "@deepseek-ai/dsh-subprocess": {
76
+ "optional": true
77
+ },
78
+ "@deepseek-ai/dsh-system-prompt": {
79
+ "optional": true
80
+ },
81
+ "@deepseek-ai/dsh-working-directory": {
82
+ "optional": true
83
+ },
84
+ "@deepseek-ai/dsh-tool-subagent": {
85
+ "optional": true
86
+ }
87
+ },
88
+ "overrides": {
89
+ "zod": "^4.6.6",
90
+ "undici": "^8.12.2"
91
+ },
28
92
  "license": "Apache-2.0",
29
93
  "author": "Xule Lin",
30
94
  "repository": {
@@ -38,6 +102,8 @@
38
102
  "keywords": [
39
103
  "claude-code",
40
104
  "codex",
105
+ "deepseek-harness",
106
+ "dsh",
41
107
  "kimi",
42
108
  "kimi-code",
43
109
  "plugin",
@@ -58,6 +124,7 @@
58
124
  "hooks/",
59
125
  "dist/",
60
126
  "plugins/kimi-codex/",
127
+ "plugins/kimi-dsh/",
61
128
  "scripts/companion.sh",
62
129
  "scripts/review-gate-hook.sh",
63
130
  "docs/safety.md",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kimi",
3
- "version": "2.4.5",
3
+ "version": "2.5.2",
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"
@@ -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 and refuses duplicate, orphaned, or malformed managed blocks.
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
- const inlineHooks = normalizeTopLevelInlineHooks(original, lineEnding);
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, lineEnding, hostId, expectedCommand);
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
- "Run `/kimi:setup --uninstall` to clean up, then re-run `/kimi:setup`.",
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, lineEnding, removeAllHosts ? undefined : hostId, expectedCommand);
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 = splitPreservingEnding(contents);
521
- const before = lines.slice(0, beginLine).join(lineEnding);
522
- const after = lines.slice(endLine + 1).join(lineEnding);
523
- const beforeWithSep = before.endsWith(lineEnding) || before.length === 0 ? before : `${before}${lineEnding}`;
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
- if (MARKERS.END_LINE_RE.test(nextTrimmed)) {
583
- endIdx = j;
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(line);
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(lines[k]);
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(line);
675
+ result.push(rawLines[i]);
623
676
  i += 1;
624
677
  continue;
625
678
  }
626
- result.push(line);
679
+ result.push(rawLines[i]);
627
680
  i += 1;
628
681
  }
629
- const collapsed = [];
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, lineEnding, ownedBy, alsoMatchCommand) {
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 = splitPreservingEnding(contents);
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: collapsed.join(lineEnding),
709
+ pruned: kept.join(""),
683
710
  count: ranges.length,
684
711
  commands: ranges.map((range) => range.command),
685
712
  };