ultra-builder-pro-cli 0.25.1 → 0.26.0

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 (241) hide show
  1. package/.ultra-template/contexts/TEMPLATE.md +87 -0
  2. package/.ultra-template/decisions/.gitkeep +1 -0
  3. package/.ultra-template/evidence/.gitkeep +1 -0
  4. package/.ultra-template/north-star.md +46 -0
  5. package/.ultra-template/research/.gitkeep +1 -0
  6. package/.ultra-template/specs/discovery.md +2 -2
  7. package/.ultra-template/tasks.json +3 -0
  8. package/.ultra-template/test-report.json +27 -0
  9. package/CHANGELOG.md +40 -0
  10. package/README.md +217 -579
  11. package/adapters/_shared/delegate-profiles/kimi-read-only.md +15 -0
  12. package/adapters/_shared/delegate-profiles/kimi-write.md +17 -0
  13. package/adapters/_shared/file-ops.cjs +76 -2
  14. package/adapters/_shared/host-profile.cjs +97 -0
  15. package/adapters/_shared/plugin-core.cjs +204 -0
  16. package/adapters/_shared/runtime-assets.cjs +31 -96
  17. package/adapters/claude.js +92 -291
  18. package/adapters/codex.js +309 -842
  19. package/adapters/grok.js +143 -1469
  20. package/adapters/kimi.js +124 -1053
  21. package/adapters/opencode.js +230 -742
  22. package/bin/delegate-worker.cjs +323 -0
  23. package/bin/delegate.cjs +456 -0
  24. package/bin/install.js +38 -11
  25. package/docs/ARCHITECTURE.md +122 -216
  26. package/docs/ARTIFACT-AUTHORITY.md +90 -268
  27. package/docs/DECISIONS.md +75 -87
  28. package/docs/PHILOSOPHY.md +177 -0
  29. package/docs/PLUGIN-ISOLATION-CONTRACT.md +62 -39
  30. package/docs/RUNTIME-COMPAT-MATRIX.md +99 -156
  31. package/docs/SKILL-AUTHORING.md +94 -73
  32. package/docs/WORKFLOW-LIFECYCLE.md +93 -189
  33. package/hooks/README.md +17 -37
  34. package/hooks/_common.py +113 -0
  35. package/hooks/adapters/codex.py +9 -16
  36. package/hooks/adapters/grok.py +5 -5
  37. package/hooks/adapters/kimi.py +8 -14
  38. package/hooks/block_dangerous_commands.py +95 -0
  39. package/hooks/compact_context.py +65 -0
  40. package/hooks/mid_workflow_recall.py +25 -0
  41. package/hooks/post_edit_guard.py +105 -0
  42. package/hooks/session_context.py +30 -0
  43. package/package.json +17 -52
  44. package/skills/ultra-change/SKILL.md +97 -50
  45. package/skills/ultra-change/references/change-contract.md +40 -1
  46. package/skills/ultra-delegate/SKILL.md +82 -0
  47. package/skills/ultra-delegate/references/delegation-contract.md +51 -0
  48. package/skills/ultra-delegate/scripts/delegate_wait.py +86 -0
  49. package/skills/ultra-deliver/SKILL.md +61 -42
  50. package/skills/ultra-deliver/references/baseline-reconciliation.md +39 -20
  51. package/skills/ultra-dev/SKILL.md +96 -62
  52. package/skills/ultra-dev/references/debugging.md +22 -0
  53. package/skills/ultra-domain-modeling/SKILL.md +79 -0
  54. package/skills/ultra-grilling/SKILL.md +79 -0
  55. package/skills/ultra-init/SKILL.md +94 -37
  56. package/skills/ultra-init/references/brownfield-adoption.md +33 -83
  57. package/skills/ultra-init/scripts/init_project.cjs +84 -0
  58. package/skills/ultra-plan/SKILL.md +88 -68
  59. package/skills/ultra-research/SKILL.md +62 -48
  60. package/skills/ultra-research/references/00-problem-validation.md +5 -5
  61. package/skills/ultra-research/references/01-opportunity-discovery.md +3 -3
  62. package/skills/ultra-research/references/02-market-assessment.md +5 -4
  63. package/skills/ultra-research/references/03-competitive-landscape.md +3 -3
  64. package/skills/ultra-research/references/04-product-strategy.md +3 -3
  65. package/skills/ultra-research/references/05-assumptions-validation.md +5 -5
  66. package/skills/ultra-research/references/10-user-personas.md +3 -2
  67. package/skills/ultra-research/references/11-user-scenarios.md +3 -3
  68. package/skills/ultra-research/references/20-user-stories.md +4 -3
  69. package/skills/ultra-research/references/21-features-scope.md +3 -2
  70. package/skills/ultra-research/references/22-success-metrics.md +3 -3
  71. package/skills/ultra-research/references/30-architecture-context.md +3 -3
  72. package/skills/ultra-research/references/31-solution-strategy.md +3 -3
  73. package/skills/ultra-research/references/32-building-blocks.md +3 -3
  74. package/skills/ultra-research/references/40-deployment.md +3 -3
  75. package/skills/ultra-research/references/41-quality-risks.md +3 -3
  76. package/skills/ultra-research/references/99-synthesis.md +7 -7
  77. package/skills/ultra-review/SKILL.md +59 -32
  78. package/skills/ultra-review/references/code.md +17 -0
  79. package/skills/ultra-review/references/comments.md +12 -0
  80. package/skills/ultra-review/references/design.md +12 -0
  81. package/skills/ultra-review/references/errors.md +13 -0
  82. package/skills/ultra-review/references/spec.md +13 -0
  83. package/skills/ultra-review/references/tests.md +12 -0
  84. package/skills/ultra-review/references/worker-packet.md +40 -0
  85. package/skills/ultra-review/scripts/review_wait.py +26 -15
  86. package/skills/ultra-status/SKILL.md +61 -36
  87. package/skills/ultra-tdd/SKILL.md +81 -0
  88. package/skills/ultra-tdd/references/templates/README.md +20 -0
  89. package/skills/ultra-tdd/references/templates/feature-flag-default-audit.sh +79 -0
  90. package/skills/ultra-tdd/references/templates/persistence-real.ts +78 -0
  91. package/skills/ultra-tdd/references/templates/testcontainer-postgres.py +78 -0
  92. package/skills/ultra-tdd/references/templates/testcontainer-postgres.ts +73 -0
  93. package/skills/ultra-tdd/references/templates/vertical-slice.ts +66 -0
  94. package/skills/ultra-tdd/references/test-execution.md +18 -0
  95. package/skills/ultra-test/SKILL.md +70 -30
  96. package/skills/ultra-test/references/export-syntax.md +18 -0
  97. package/skills/ultra-test/scripts/worktree_digest.cjs +54 -0
  98. package/skills/ultra-think/SKILL.md +49 -27
  99. package/skills/ultra-think/references/autonomy-boundary.md +48 -0
  100. package/skills/ultra-think/references/interaction-boundary.md +18 -33
  101. package/.ultra-template/docs/research/README.md +0 -55
  102. package/.ultra-template/reports/templates/delivery-report.json +0 -17
  103. package/.ultra-template/reports/templates/test-report.json +0 -18
  104. package/.ultra-template/tasks/tasks.json +0 -15
  105. package/.ultra-template/templates/task-context.md +0 -19
  106. package/adapters/_shared/codex-assets.cjs +0 -725
  107. package/adapters/_shared/codex-ultra-tools-entry.cjs +0 -11
  108. package/adapters/_shared/interaction-contract.cjs +0 -129
  109. package/adapters/_shared/path-rewrite.cjs +0 -79
  110. package/adapters/_shared/runtime-native.cjs +0 -132
  111. package/adapters/_shared/settings-merge.cjs +0 -76
  112. package/agents/code-reviewer.md +0 -42
  113. package/agents/debugger.md +0 -40
  114. package/agents/review-code.md +0 -49
  115. package/agents/review-comments.md +0 -38
  116. package/agents/review-coordinator.md +0 -56
  117. package/agents/review-design.md +0 -40
  118. package/agents/review-errors.md +0 -43
  119. package/agents/review-spec.md +0 -39
  120. package/agents/review-tests.md +0 -40
  121. package/agents/tdd-runner.md +0 -38
  122. package/bin/orchestrator.js +0 -383
  123. package/commands/ultra-change.md +0 -10
  124. package/commands/ultra-deliver.md +0 -10
  125. package/commands/ultra-dev.md +0 -10
  126. package/commands/ultra-doctor.md +0 -10
  127. package/commands/ultra-init.md +0 -10
  128. package/commands/ultra-plan.md +0 -10
  129. package/commands/ultra-research.md +0 -10
  130. package/commands/ultra-review.md +0 -10
  131. package/commands/ultra-status.md +0 -10
  132. package/commands/ultra-test.md +0 -10
  133. package/commands/ultra-think.md +0 -10
  134. package/docs/AGENT-CONTEXT.md +0 -191
  135. package/docs/COMMIT-HASH-BACKFILL.md +0 -51
  136. package/docs/LEGACY-CLI-CROSSWALK.md +0 -175
  137. package/docs/ROADMAP.md +0 -101
  138. package/docs/STATE-DB-ACCESS-POLICY.md +0 -161
  139. package/hooks/active_task_context.py +0 -152
  140. package/hooks/context_envelope.py +0 -100
  141. package/hooks/health_check.py +0 -196
  142. package/hooks/pre_stop_check.py +0 -66
  143. package/hooks/runtime_paths.py +0 -396
  144. package/hooks/subagent_tracker.py +0 -88
  145. package/hooks/workflow_checkpoint.py +0 -76
  146. package/hooks/workflow_context.py +0 -48
  147. package/hooks/workflow_resume.py +0 -84
  148. package/mcp-server/hook-context.cjs +0 -38
  149. package/mcp-server/hook-event.cjs +0 -34
  150. package/mcp-server/lib/archive-journal.cjs +0 -1119
  151. package/mcp-server/lib/archive-mutation-worker.py +0 -365
  152. package/mcp-server/lib/artifact-registry.cjs +0 -1232
  153. package/mcp-server/lib/baseline-workflow.cjs +0 -1193
  154. package/mcp-server/lib/canonical-json.cjs +0 -19
  155. package/mcp-server/lib/change-authority.cjs +0 -26
  156. package/mcp-server/lib/change-packet.cjs +0 -790
  157. package/mcp-server/lib/change-workflow.cjs +0 -65
  158. package/mcp-server/lib/context-envelope.cjs +0 -581
  159. package/mcp-server/lib/context-paths.cjs +0 -142
  160. package/mcp-server/lib/decision-records.cjs +0 -423
  161. package/mcp-server/lib/delivery-transaction.cjs +0 -471
  162. package/mcp-server/lib/doctor-backup-worker.cjs +0 -328
  163. package/mcp-server/lib/doctor.cjs +0 -627
  164. package/mcp-server/lib/git-bootstrap.cjs +0 -651
  165. package/mcp-server/lib/init-project.cjs +0 -1021
  166. package/mcp-server/lib/kernel-change-workflow.cjs +0 -2029
  167. package/mcp-server/lib/legacy-change-workflow.cjs +0 -3032
  168. package/mcp-server/lib/managed-file-write.cjs +0 -172
  169. package/mcp-server/lib/plan-checkpoint.cjs +0 -177
  170. package/mcp-server/lib/plan-publication-worker.cjs +0 -830
  171. package/mcp-server/lib/plan-store.cjs +0 -794
  172. package/mcp-server/lib/prd-parser.cjs +0 -83
  173. package/mcp-server/lib/pricing.cjs +0 -44
  174. package/mcp-server/lib/project-context.cjs +0 -178
  175. package/mcp-server/lib/projector.cjs +0 -386
  176. package/mcp-server/lib/provider-refs.cjs +0 -42
  177. package/mcp-server/lib/runtime-paths.cjs +0 -1714
  178. package/mcp-server/lib/runtime-state.cjs +0 -222
  179. package/mcp-server/lib/safe-project-file.cjs +0 -385
  180. package/mcp-server/lib/stage-checkpoints.cjs +0 -463
  181. package/mcp-server/lib/state-authority.cjs +0 -95
  182. package/mcp-server/lib/state-db.cjs +0 -1580
  183. package/mcp-server/lib/state-ops.cjs +0 -1428
  184. package/mcp-server/lib/task-contract.cjs +0 -404
  185. package/mcp-server/lib/task-expander.cjs +0 -105
  186. package/mcp-server/lib/task-ledger.cjs +0 -1964
  187. package/mcp-server/lib/telemetry.cjs +0 -85
  188. package/mcp-server/lib/topo.cjs +0 -131
  189. package/mcp-server/lib/ultra-facade.cjs +0 -2684
  190. package/mcp-server/lib/worker-packet.cjs +0 -358
  191. package/mcp-server/server.cjs +0 -437
  192. package/orchestrator/auto-merge.cjs +0 -128
  193. package/orchestrator/daemon.cjs +0 -264
  194. package/orchestrator/dispatch-rules.cjs +0 -78
  195. package/orchestrator/parallel-orchestrator.cjs +0 -903
  196. package/orchestrator/planner/plan-builder.cjs +0 -125
  197. package/orchestrator/recovery.cjs +0 -188
  198. package/orchestrator/session-close-journal-worker.cjs +0 -5
  199. package/orchestrator/session-close-journal.cjs +0 -1014
  200. package/orchestrator/session-runner.cjs +0 -1153
  201. package/orchestrator/worktree-manager.cjs +0 -282
  202. package/settings.json +0 -7
  203. package/skills/cc-collab/SKILL.md +0 -42
  204. package/skills/code-review-expert/SKILL.md +0 -43
  205. package/skills/code-review-expert/references/correctness-reliability.md +0 -30
  206. package/skills/code-review-expert/references/design-boundaries.md +0 -26
  207. package/skills/code-review-expert/references/removal-plan.md +0 -24
  208. package/skills/codex-collab/SKILL.md +0 -49
  209. package/skills/codex-collab/evals/evals.json +0 -35
  210. package/skills/integration-rules/SKILL.md +0 -32
  211. package/skills/security-rules/SKILL.md +0 -38
  212. package/skills/testing-rules/SKILL.md +0 -32
  213. package/skills/ultra-doctor/SKILL.md +0 -41
  214. package/skills/ultra-plan/references/semantic-preflight.md +0 -20
  215. package/skills/ultra-verify/SKILL.md +0 -46
  216. package/skills/ultra-verify/evals/evals.json +0 -46
  217. package/skills/ultra-verify/references/cross-verify-modes.md +0 -28
  218. package/skills/ultra-verify/references/evidence-status.md +0 -21
  219. package/skills/ultra-verify/scripts/verify_wait.py +0 -94
  220. package/spec/cli-protocol.md +0 -102
  221. package/spec/mcp-tools.yaml +0 -312
  222. package/spec/schemas/baseline-reconciliation.v1.schema.json +0 -73
  223. package/spec/schemas/change-delta.v1.schema.json +0 -153
  224. package/spec/schemas/command-manifest.schema.json +0 -29
  225. package/spec/schemas/context-file.v4.5.schema.json +0 -54
  226. package/spec/schemas/delivery-report.v1.schema.json +0 -66
  227. package/spec/schemas/documentation-reconciliation.v1.schema.json +0 -106
  228. package/spec/schemas/mcp-tools.schema.json +0 -136
  229. package/spec/schemas/skill-manifest.schema.json +0 -24
  230. package/spec/schemas/state-db.sql +0 -641
  231. package/spec/schemas/task-ledger.v2.schema.json +0 -296
  232. package/spec/schemas/tasks.v4.5.schema.json +0 -96
  233. package/spec/schemas/test-report.v1.schema.json +0 -225
  234. package/ultra-tools/cli.cjs +0 -129
  235. package/ultra-tools/commands/db.cjs +0 -226
  236. package/ultra-tools/commands/legacy-memory.cjs +0 -159
  237. package/ultra-tools/commands/migrate.cjs +0 -1386
  238. package/ultra-tools/commands/session.cjs +0 -213
  239. package/ultra-tools/commands/status.cjs +0 -559
  240. package/ultra-tools/commands/system.cjs +0 -498
  241. package/ultra-tools/commands/task.cjs +0 -105
package/adapters/kimi.js CHANGED
@@ -1,857 +1,80 @@
1
1
  'use strict';
2
2
 
3
- /** Build and register a Kimi Code 0.26+ native Ultra Builder Pro plugin. */
4
-
5
3
  const fs = require('node:fs');
6
- const crypto = require('node:crypto');
7
4
  const os = require('node:os');
8
5
  const path = require('node:path');
9
- const { spawnSync } = require('node:child_process');
10
6
 
11
7
  const {
12
- copyTree,
13
- ensureDir,
14
- isManaged,
8
+ copyHooks,
9
+ copySkills,
10
+ inspectPlugin,
15
11
  markManaged,
12
+ publishManagedTrees,
13
+ removeManaged,
14
+ writePluginProvenance,
15
+ } = require('./_shared/plugin-core.cjs');
16
+ const {
17
+ captureAbsent,
18
+ ensureDir,
19
+ managedMetadata,
20
+ pruneCreatedEmpty,
16
21
  removeTree,
17
22
  writeAtomic,
18
23
  } = require('./_shared/file-ops.cjs');
19
- const {
20
- applyNativeDoctor,
21
- buildMcpRuntime,
22
- } = require('./_shared/codex-assets.cjs');
23
- const { parse: parseFm, serialize: serializeFm } = require('./_shared/frontmatter.cjs');
24
- const { adaptInteractionGuidance } = require('./_shared/interaction-contract.cjs');
25
- const provenance = require('./_shared/provenance.cjs');
26
- const { PUBLIC_TOOLS } = require('../mcp-server/lib/ultra-facade.cjs');
27
- const {
28
- CORE_PUBLIC_SKILLS,
29
- WORKFLOW_HOOK_FILES,
30
- skillPolicy,
31
- skillsForRuntime,
32
- } = require('./_shared/runtime-assets.cjs');
33
24
 
34
25
  const PLUGIN_ID = 'ultra-builder-pro';
35
- const MCP_SERVER_NAME = PLUGIN_ID;
36
- const PROVENANCE_FILE = 'provenance.json';
37
- const REGISTRY_RELATIVE = path.join('plugins', 'installed.json');
38
- const MANAGED_RELATIVE = path.join('plugins', 'managed', PLUGIN_ID);
39
- const KIMI_PLUGIN_PROBE = String.raw`
40
- 'use strict';
41
- const { spawn } = require('node:child_process');
42
-
43
- const kimiBin = process.argv[1];
44
- const pluginRoot = process.argv[2];
45
- const pluginId = process.argv[3];
46
- const sessionCwd = process.argv[4];
47
- const kimiHome = process.argv[5];
48
- const probeTimeoutMs = Number(process.argv[6]);
49
- const cleanupHttpTimeoutMs = Number(process.argv[7]);
50
- const termWaitMs = Number(process.argv[8]);
51
- const killWaitMs = Number(process.argv[9]);
52
- let child = null;
53
- let activeBaseUrl = '';
54
- let activeHeaders = {};
55
- let activeSessionId = '';
56
- let cleanupPromise = null;
57
-
58
- async function readJson(url, headers, options = {}, attempts = 1) {
59
- let lastError;
60
- for (let attempt = 0; attempt < attempts; attempt += 1) {
61
- try {
62
- const response = await fetch(url, { ...options, headers: {
63
- ...headers,
64
- ...(options.headers || {}),
65
- } });
66
- if (!response.ok) throw new Error('HTTP ' + response.status);
67
- return await response.json();
68
- } catch (error) {
69
- lastError = error;
70
- if (attempt + 1 < attempts) {
71
- await new Promise((resolve) => setTimeout(resolve, 100));
72
- }
73
- }
74
- }
75
- throw lastError;
76
- }
77
-
78
- function childExited() {
79
- return !child || child.exitCode !== null || child.signalCode !== null;
80
- }
81
-
82
- async function waitForExitWithin(timeoutMs) {
83
- if (childExited()) return true;
84
- return await new Promise((resolve) => {
85
- const onExit = () => {
86
- clearTimeout(timer);
87
- resolve(true);
88
- };
89
- const timer = setTimeout(() => {
90
- child.removeListener('exit', onExit);
91
- resolve(childExited());
92
- }, timeoutMs);
93
- child.once('exit', onExit);
94
- });
95
- }
96
-
97
- function signalChild(signal) {
98
- if (childExited()) return;
99
- if (process.platform !== 'win32' && child.pid) {
100
- try {
101
- process.kill(-child.pid, signal);
102
- return;
103
- } catch (error) {
104
- if (error.code !== 'ESRCH') throw error;
105
- }
106
- }
107
- try {
108
- child.kill(signal);
109
- } catch (error) {
110
- if (error.code !== 'ESRCH') throw error;
111
- }
112
- }
113
-
114
- async function stopChild() {
115
- if (!child || child.exitCode !== null || child.signalCode !== null) return;
116
- signalChild('SIGTERM');
117
- if (!await waitForExitWithin(termWaitMs)) {
118
- signalChild('SIGKILL');
119
- if (!await waitForExitWithin(killWaitMs)) {
120
- throw new Error('Kimi probe process group did not exit after SIGKILL');
121
- }
122
- }
123
- }
124
-
125
- async function cleanup() {
126
- if (cleanupPromise) return cleanupPromise;
127
- cleanupPromise = (async () => {
128
- let cleanupError = null;
129
- if (activeBaseUrl && activeSessionId) {
130
- try {
131
- const archived = await readJson(
132
- activeBaseUrl + '/api/v1/sessions/' + encodeURIComponent(activeSessionId) + ':archive',
133
- activeHeaders,
134
- {
135
- method: 'POST',
136
- headers: { 'content-type': 'application/json' },
137
- body: '{}',
138
- signal: AbortSignal.timeout(cleanupHttpTimeoutMs),
139
- },
140
- );
141
- if (archived.code !== 0 || archived.data?.archived !== true) {
142
- throw new Error('Kimi failed to archive probe session ' + activeSessionId);
143
- }
144
- activeSessionId = '';
145
- } catch (error) {
146
- cleanupError = error;
147
- }
148
- }
149
- if (activeBaseUrl) {
150
- try {
151
- await fetch(activeBaseUrl + '/api/v1/shutdown', {
152
- method: 'POST',
153
- headers: activeHeaders,
154
- signal: AbortSignal.timeout(cleanupHttpTimeoutMs),
155
- });
156
- } catch (_) {
157
- // The server may close the socket before returning its shutdown response.
158
- }
159
- }
160
- try {
161
- await stopChild();
162
- } catch (error) {
163
- cleanupError ||= error;
164
- }
165
- if (cleanupError) {
166
- throw new Error('KIMI_PLUGIN_PROBE_TIMEOUT: ' + cleanupError.message);
167
- }
168
- })();
169
- return cleanupPromise;
170
- }
171
-
172
- async function main() {
173
- let stdout = '';
174
- let stderr = '';
175
- child = spawn(kimiBin, [
176
- 'web',
177
- '--port', '0',
178
- '--host', '127.0.0.1',
179
- '--no-open',
180
- '--debug-endpoints',
181
- ], {
182
- cwd: sessionCwd,
183
- env: { ...process.env, KIMI_CODE_HOME: kimiHome },
184
- detached: process.platform !== 'win32',
185
- stdio: ['ignore', 'pipe', 'pipe'],
186
- });
187
- child.stdout.on('data', (chunk) => { stdout += chunk; });
188
- child.stderr.on('data', (chunk) => { stderr += chunk; });
189
-
190
- try {
191
- let token = null;
192
- let baseUrl = null;
193
- for (let attempt = 0; attempt < 200 && (!token || !baseUrl); attempt += 1) {
194
- token = stdout.match(/Token:\s+(\S+)/)?.[1] || null;
195
- baseUrl = stdout.match(/Local:\s+(http:\/\/127\.0\.0\.1:\d+)/)?.[1] || null;
196
- if (!token || !baseUrl) await new Promise((resolve) => setTimeout(resolve, 100));
197
- }
198
- if (!token) throw new Error('Kimi web did not publish a loopback bearer token');
199
- if (!baseUrl) throw new Error('Kimi web did not publish its loopback URL');
200
- const headers = { Authorization: 'Bearer ' + token };
201
- activeBaseUrl = baseUrl;
202
- activeHeaders = headers;
203
- await readJson(baseUrl + '/api/v1/debug/channels', headers, {}, 200);
204
- const response = await readJson(
205
- baseUrl + '/api/v1/debug/pluginService/listPlugins',
206
- headers,
207
- );
208
- if (response.code !== 0 || !Array.isArray(response.data)) {
209
- throw new Error('Kimi pluginService.listPlugins returned an invalid response');
210
- }
211
- const plugin = response.data.find((entry) => entry && entry.id === pluginId);
212
- const mcpResponse = await readJson(
213
- baseUrl + '/api/v1/debug/pluginService/enabledMcpServers',
214
- headers,
215
- );
216
- if (mcpResponse.code !== 0 || !mcpResponse.data || typeof mcpResponse.data !== 'object') {
217
- throw new Error('Kimi pluginService.enabledMcpServers returned an invalid response');
218
- }
219
- const sessionResponse = await readJson(
220
- baseUrl + '/api/v1/sessions',
221
- headers,
222
- {
223
- method: 'POST',
224
- headers: { 'content-type': 'application/json' },
225
- body: JSON.stringify({ metadata: { cwd: sessionCwd } }),
226
- },
227
- );
228
- const sessionId = sessionResponse.data?.id;
229
- if (sessionResponse.code !== 0 || !sessionId) {
230
- throw new Error('Kimi failed to create a probe session');
231
- }
232
- activeSessionId = sessionId;
233
- const agentBase = baseUrl
234
- + '/api/v1/debug/session/' + encodeURIComponent(sessionId)
235
- + '/agent/main/agentMcpService/';
236
- const initialLoad = await readJson(
237
- agentBase + 'waitForInitialLoad',
238
- headers,
239
- );
240
- if (initialLoad.code !== 0) {
241
- throw new Error('Kimi agentMcpService.waitForInitialLoad failed');
242
- }
243
- const connectionResponse = await readJson(agentBase + 'list', headers);
244
- if (connectionResponse.code !== 0 || !Array.isArray(connectionResponse.data)) {
245
- throw new Error('Kimi agentMcpService.list returned an invalid response');
246
- }
247
- const toolsResponse = await readJson(
248
- baseUrl + '/api/v1/tools?session_id=' + encodeURIComponent(sessionId),
249
- headers,
250
- );
251
- if (toolsResponse.code !== 0 || !Array.isArray(toolsResponse.data?.tools)) {
252
- throw new Error('Kimi session tool registry returned an invalid response');
253
- }
254
- process.stdout.write(JSON.stringify({
255
- plugin,
256
- mcpServers: mcpResponse.data,
257
- mcpConnections: connectionResponse.data,
258
- tools: toolsResponse.data.tools,
259
- }));
260
- } catch (error) {
261
- const detail = stderr.trim();
262
- throw new Error(error.message + (detail ? ': ' + detail : ''));
263
- } finally {
264
- await cleanup();
265
- }
266
- }
267
-
268
- let handlingSignal = false;
269
- for (const signal of ['SIGINT', 'SIGTERM']) {
270
- process.on(signal, () => {
271
- if (handlingSignal) return;
272
- handlingSignal = true;
273
- void cleanup().finally(() => process.exit(1));
274
- });
275
- }
276
-
277
- let watchdogFired = false;
278
- const watchdog = setTimeout(() => {
279
- watchdogFired = true;
280
- void cleanup().catch(() => {}).finally(() => {
281
- process.stderr.write('KIMI_PLUGIN_PROBE_TIMEOUT');
282
- process.exit(124);
283
- });
284
- }, probeTimeoutMs);
285
-
286
- main().then(() => {
287
- clearTimeout(watchdog);
288
- }).catch((error) => {
289
- clearTimeout(watchdog);
290
- if (watchdogFired) return;
291
- process.stderr.write(error.stack || error.message);
292
- process.exitCode = 1;
293
- });
294
- `;
295
26
 
296
27
  function resolveTarget(ctx = {}) {
297
28
  if (ctx.configDir) return path.resolve(ctx.configDir);
298
- if (ctx.scope === 'global') {
299
- return path.resolve(process.env.KIMI_CODE_HOME || path.join(ctx.homeDir || os.homedir(), '.kimi-code'));
300
- }
29
+ if (ctx.scope === 'global') return process.env.KIMI_CODE_HOME || path.join(ctx.homeDir || os.homedir(), '.kimi-code');
301
30
  return path.join(ctx.cwd || process.cwd(), '.kimi-code');
302
31
  }
303
32
 
304
33
  function resolvePluginRoot(ctx = {}) {
305
- return path.join(resolveTarget(ctx), MANAGED_RELATIVE);
34
+ return path.join(resolveTarget(ctx), 'plugins', 'managed', PLUGIN_ID);
306
35
  }
307
36
 
308
37
  function resolveRegistryFile(ctx = {}) {
309
- return path.join(resolveTarget(ctx), REGISTRY_RELATIVE);
38
+ return path.join(resolveTarget(ctx), 'plugins', 'installed.json');
310
39
  }
311
40
 
312
41
  function resolveRepoRoot(ctx = {}) {
313
- return path.resolve(ctx.repoRoot || path.join(__dirname, '..'));
314
- }
315
-
316
- function shouldRunHostCli(ctx = {}) {
317
- if (typeof ctx.runHostCli === 'boolean') return ctx.runHostCli;
318
- return ctx.scope === 'global' && !ctx.configDir;
319
- }
320
-
321
- function countSkillRoots(pluginRoot) {
322
- const root = path.join(pluginRoot, 'skills');
323
- if (!fs.existsSync(root)) return 0;
324
- return fs.readdirSync(root, { withFileTypes: true })
325
- .filter((entry) => (
326
- entry.isDirectory()
327
- && fs.existsSync(path.join(root, entry.name, 'SKILL.md'))
328
- ))
329
- .length;
330
- }
331
-
332
- function probeKimiPlugin(ctx = {}, consumer = {}) {
333
- const probeTimeoutMs = ctx.hostCliTimeoutMs || 45000;
334
- const cleanupHttpTimeoutMs = Math.min(
335
- 1000,
336
- Math.max(10, Math.floor(probeTimeoutMs / 8)),
337
- );
338
- const termWaitMs = Math.min(1000, Math.max(10, Math.floor(probeTimeoutMs / 8)));
339
- const killWaitMs = Math.min(1000, Math.max(10, Math.floor(probeTimeoutMs / 8)));
340
- const outerTimeoutMs = probeTimeoutMs
341
- + (cleanupHttpTimeoutMs * 2)
342
- + termWaitMs
343
- + killWaitMs
344
- + 250;
345
- const result = spawnSync(
346
- process.execPath,
347
- [
348
- '-e',
349
- KIMI_PLUGIN_PROBE,
350
- ctx.kimiBin || 'kimi',
351
- consumer.pluginRoot || resolvePluginRoot(ctx),
352
- PLUGIN_ID,
353
- path.resolve(ctx.cwd || process.cwd()),
354
- consumer.kimiHome || resolveTarget(ctx),
355
- String(probeTimeoutMs),
356
- String(cleanupHttpTimeoutMs),
357
- String(termWaitMs),
358
- String(killWaitMs),
359
- ],
360
- {
361
- encoding: 'utf8',
362
- stdio: ['ignore', 'pipe', 'pipe'],
363
- killSignal: 'SIGKILL',
364
- env: {
365
- ...process.env,
366
- },
367
- timeout: outerTimeoutMs,
368
- },
369
- );
370
- if (result.error) {
371
- if (result.error.code === 'ETIMEDOUT') {
372
- const error = new Error('Kimi native plugin probe timed out');
373
- error.code = 'HOST_CLI_TIMEOUT';
374
- throw error;
375
- }
376
- throw result.error;
377
- }
378
- if (result.status === 124 || result.stderr.includes('KIMI_PLUGIN_PROBE_TIMEOUT')) {
379
- const error = new Error('Kimi native plugin probe timed out');
380
- error.code = 'HOST_CLI_TIMEOUT';
381
- throw error;
382
- }
383
- if (result.status !== 0) {
384
- const detail = (result.stderr || result.stdout || '').trim();
385
- throw new Error(`Kimi native plugin probe failed${detail ? `: ${detail}` : ''}`);
386
- }
387
- const payload = JSON.parse(result.stdout);
388
- return payload;
42
+ return ctx.repoRoot || path.resolve(__dirname, '..');
389
43
  }
390
44
 
391
- function stableHash8(input) {
392
- let hash = 2166136261;
393
- for (let index = 0; index < input.length; index += 1) {
394
- hash ^= input.codePointAt(index);
395
- hash = Math.trunc(Math.imul(hash, 16777619));
45
+ function assertPluginScope(ctx = {}) {
46
+ if (ctx.scope === 'local' && !ctx.configDir) {
47
+ throw new Error('Kimi plugins are user-scoped; use global scope or an isolated configDir');
396
48
  }
397
- return hash.toString(16).padStart(8, '0');
398
49
  }
399
50
 
400
- function sanitizeKimiMcpNamePart(value) {
401
- return value.replaceAll(/[^a-zA-Z0-9_-]/g, '_').replaceAll(/_+/g, '_');
402
- }
403
-
404
- function matchesQualifiedKimiToolName(observed, serverId, publicName) {
405
- if (typeof observed !== 'string') return false;
406
- const full = `mcp__${sanitizeKimiMcpNamePart(serverId)}__${sanitizeKimiMcpNamePart(publicName)}`;
407
- if (observed === full) return true;
408
- const truncated = /^(.*)_(-?[0-9a-f]{8})$/.exec(observed);
409
- return !!truncated
410
- && truncated[2] === stableHash8(full)
411
- && truncated[1] === full.slice(0, truncated[1].length);
412
- }
413
-
414
- function normalizeKimiToolIdentity(descriptor, serverId, expectedTools) {
415
- if (typeof serverId !== 'string' || !serverId) return null;
416
- const matches = expectedTools.filter((name) => (
417
- matchesQualifiedKimiToolName(descriptor?.name, serverId, name)
418
- ));
419
- return matches.length === 1 ? matches[0] : null;
420
- }
421
-
422
- function sameStringSet(actual, expected) {
423
- if (!Array.isArray(actual) || actual.length !== expected.length) return false;
424
- const actualSet = new Set(actual);
425
- return actualSet.size === expected.length
426
- && expected.every((value) => actualSet.has(value));
427
- }
428
-
429
- function inspectActualKimiRegistration(ctx = {}) {
430
- const registryFile = resolveRegistryFile(ctx);
431
- const pluginRoot = resolvePluginRoot(ctx);
432
- if (!fs.existsSync(registryFile)) {
433
- return {
434
- registryFile,
435
- pluginRoot,
436
- registryBytes: null,
437
- record: null,
438
- registryOk: false,
439
- pluginRootOk: false,
440
- };
441
- }
442
- const registryBytes = fs.readFileSync(registryFile);
443
- const registry = loadRegistry(registryFile);
444
- const record = registry.plugins.find((entry) => entry && entry.id === PLUGIN_ID) || null;
445
- const registryOk = !!record
446
- && path.resolve(record.root || '') === path.resolve(pluginRoot)
447
- && record.source === 'local-path'
448
- && record.enabled === true;
449
- let pluginRootOk = false;
450
- if (registryOk) {
451
- try {
452
- const stat = fs.lstatSync(pluginRoot);
453
- pluginRootOk = stat.isDirectory()
454
- && !stat.isSymbolicLink()
455
- && isManaged(pluginRoot)
456
- && fs.existsSync(path.join(pluginRoot, 'kimi.plugin.json'));
457
- } catch (error) {
458
- if (error.code !== 'ENOENT') throw error;
459
- }
460
- }
461
- return {
462
- registryFile,
463
- pluginRoot,
464
- registryBytes,
465
- record,
466
- registryOk,
467
- pluginRootOk,
468
- };
469
- }
470
-
471
- function createIsolatedKimiRegistry(actual) {
472
- const kimiHome = fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-kimi-doctor-'));
473
- const registryFile = path.join(kimiHome, REGISTRY_RELATIVE);
474
- ensureDir(path.dirname(registryFile));
475
- fs.writeFileSync(registryFile, actual.registryBytes);
476
- if (!fs.readFileSync(registryFile).equals(actual.registryBytes)) {
477
- removeTree(kimiHome);
478
- throw new Error('failed to create a byte-for-byte Kimi registry clone');
479
- }
480
- return { kimiHome, registryFile };
481
- }
482
-
483
- function invalidActualKimiRegistration(actual) {
484
- const error = new Error(
485
- `Kimi actual registration is invalid: ${actual.registryFile} -> ${actual.pluginRoot}`,
486
- );
487
- error.code = 'ACTUAL_KIMI_REGISTRATION_INVALID';
488
- error.actual = actual;
489
- return error;
490
- }
491
-
492
- function inspectKimiHost(ctx = {}) {
493
- const actual = inspectActualKimiRegistration(ctx);
494
- if (!actual.registryOk || !actual.pluginRootOk || !actual.registryBytes) {
495
- throw invalidActualKimiRegistration(actual);
496
- }
497
- const target = actual.pluginRoot;
498
- const manifest = JSON.parse(fs.readFileSync(path.join(target, 'kimi.plugin.json'), 'utf8'));
499
- const isolated = createIsolatedKimiRegistry(actual);
500
- try {
501
- const doctorResult = spawnSync(ctx.kimiBin || 'kimi', ['doctor'], {
502
- encoding: 'utf8',
503
- timeout: ctx.hostCliTimeoutMs || 30000,
504
- killSignal: 'SIGKILL',
505
- stdio: ['ignore', 'pipe', 'pipe'],
506
- env: {
507
- ...process.env,
508
- KIMI_CODE_HOME: isolated.kimiHome,
509
- },
510
- });
511
- if (doctorResult.error) {
512
- if (doctorResult.error.code === 'ETIMEDOUT') {
513
- const error = new Error('kimi doctor timed out');
514
- error.code = 'HOST_CLI_TIMEOUT';
515
- throw error;
516
- }
517
- throw doctorResult.error;
518
- }
519
- if (doctorResult.status !== 0) {
520
- const detail = (doctorResult.stderr || doctorResult.stdout || '').trim();
521
- throw new Error(`kimi doctor failed${detail ? `: ${detail}` : ''}`);
522
- }
523
- if (!fs.readFileSync(isolated.registryFile).equals(actual.registryBytes)) {
524
- throw new Error('isolated Kimi doctor mutated its registry clone');
525
- }
526
- const probe = probeKimiPlugin(ctx, {
527
- kimiHome: isolated.kimiHome,
528
- pluginRoot: actual.pluginRoot,
529
- });
530
- const plugin = probe.plugin || null;
531
- const expected = {
532
- skillCount: countSkillRoots(target),
533
- mcpServerCount: Object.keys(manifest.mcpServers || {}).length,
534
- hookCount: Array.isArray(manifest.hooks) ? manifest.hooks.length : 0,
535
- commandCount: fs.readdirSync(path.join(target, 'commands'))
536
- .filter((name) => name.endsWith('.md')).length,
537
- };
538
- const pluginOk = !!plugin
539
- && plugin.enabled === true
540
- && plugin.state === 'ok'
541
- && plugin.hasErrors === false
542
- && plugin.version === manifest.version
543
- && plugin.skillCount === expected.skillCount
544
- && plugin.hookCount === expected.hookCount
545
- && plugin.commandCount === expected.commandCount;
546
- const nativeServerName = `plugin-${PLUGIN_ID}:${MCP_SERVER_NAME}`;
547
- const nativeMcp = probe.mcpServers?.[nativeServerName];
548
- const mcpConnection = (probe.mcpConnections || []).find((entry) => (
549
- entry?.name === nativeServerName
550
- ));
551
- const enabledTools = manifest.mcpServers?.[MCP_SERVER_NAME]?.enabledTools;
552
- const expectedTools = [...PUBLIC_TOOLS];
553
- const connectionToolServerId = mcpConnection?.name?.replaceAll(':', '_');
554
- const connectedTools = (probe.tools || []).filter((entry) => (
555
- entry?.source === 'mcp'
556
- && entry.mcp_server_id === connectionToolServerId
557
- && entry.active !== false
558
- ));
559
- const normalizedTools = connectedTools.map((entry) => (
560
- normalizeKimiToolIdentity(entry, connectionToolServerId, expectedTools)
561
- ));
562
- const mcpOk = pluginOk
563
- && plugin.mcpServerCount === expected.mcpServerCount
564
- && plugin.enabledMcpServerCount === expected.mcpServerCount
565
- && nativeMcp?.enabled === true
566
- && path.resolve(nativeMcp.cwd || '') === path.resolve(target)
567
- && nativeMcp.command === manifest.mcpServers?.[MCP_SERVER_NAME]?.command
568
- && JSON.stringify(nativeMcp.args) === JSON.stringify(
569
- manifest.mcpServers?.[MCP_SERVER_NAME]?.args,
570
- )
571
- && sameStringSet(nativeMcp.enabledTools, expectedTools)
572
- && sameStringSet(enabledTools, expectedTools)
573
- && mcpConnection?.status === 'connected'
574
- && Number.isInteger(mcpConnection.toolCount)
575
- && mcpConnection.toolCount === expectedTools.length
576
- && normalizedTools.every((name) => name !== null)
577
- && sameStringSet(normalizedTools, expectedTools);
578
- return {
579
- stdout: doctorResult.stdout,
580
- plugin,
581
- pluginOk,
582
- mcpOk,
583
- nativeMcp,
584
- mcpConnection,
585
- expected,
586
- actualRegistry: {
587
- file: actual.registryFile,
588
- bytes: actual.registryBytes.length,
589
- sha256: crypto.createHash('sha256').update(actual.registryBytes).digest('hex'),
590
- },
591
- actualPluginRoot: actual.pluginRoot,
592
- isolatedConsumer: { kimiHome: isolated.kimiHome },
593
- };
594
- } finally {
595
- const unchanged = fs.existsSync(actual.registryFile)
596
- && fs.readFileSync(actual.registryFile).equals(actual.registryBytes);
597
- removeTree(isolated.kimiHome);
598
- if (!unchanged) {
599
- const error = new Error(`Kimi doctor mutated the actual registry: ${actual.registryFile}`);
600
- error.code = 'ACTUAL_KIMI_REGISTRY_MUTATED';
601
- throw error;
602
- }
603
- }
604
- }
605
-
606
- function loadRegistry(file) {
607
- if (!fs.existsSync(file)) return { version: 1, plugins: [] };
608
- let parsed;
609
- try {
610
- parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
611
- } catch (error) {
612
- throw new Error(`Failed to parse ${file}: ${error.message}`);
613
- }
614
- if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)
615
- || parsed.version !== 1 || !Array.isArray(parsed.plugins)) {
616
- throw new Error(`${file} is not a valid Kimi installed.json document`);
617
- }
618
- return parsed;
619
- }
620
-
621
- function kimiTextTransform(input, assetName = '') {
622
- let text = adaptInteractionGuidance(input, 'kimi');
623
- text = text.replaceAll('$CLAUDE_PLUGIN_ROOT', '$KIMI_PLUGIN_ROOT');
624
- text = text.replaceAll('~/.claude/skills', '~/.kimi-code/skills');
625
- text = text.replaceAll('~/.claude/hooks', '~/.kimi-code/hooks');
626
- text = text.replaceAll('~/.claude', '~/.kimi-code');
627
- text = text.replaceAll('~/.codex', '~/.kimi-code');
628
- text = text.replaceAll('~/.config/opencode', '~/.kimi-code');
629
- text = text.replaceAll('CLAUDE.md', 'AGENTS.md');
630
- text = text.replace(/@skills\/([a-z0-9-]+)\/SKILL\.md/g, '$KIMI_PLUGIN_ROOT/skills/$1/SKILL.md');
631
-
632
- if (assetName === 'codex-collab') {
633
- text = text.replaceAll('the current host', 'Kimi Code');
634
- text = text.replaceAll('current host', 'Kimi Code');
635
- text = text.replaceAll('Claude Code remains primary', 'Kimi Code remains primary');
636
- text = text.replaceAll('one Claude Code-owned conclusion', 'one Kimi Code-owned conclusion');
637
- }
638
- if (assetName === 'ultra-verify') {
639
- text = text.replaceAll('the current host', 'Kimi Code');
640
- text = text.replaceAll('current host', 'Kimi Code');
641
- text = text.replaceAll('Claude Code', 'Kimi Code');
642
- text = text.replaceAll('Claude', 'Kimi');
643
- text = text.replaceAll('claude-analysis.md', 'kimi-analysis.md');
644
- }
645
- if (assetName === 'cc-collab') {
646
- text = text.replaceAll('the current host', 'Kimi Code');
647
- text = text.replaceAll('current host', 'Kimi Code');
648
- }
649
- if (assetName === 'ultra-review') {
650
- text = text.replace(
651
- /(?:the current host's native|the host-native) bounded-worker\s+mechanism/g,
652
- 'Kimi `AgentSwarm` for parallel reviewers or one foreground Kimi `Agent` for a single reviewer, using the worker prompt files under `$KIMI_PLUGIN_ROOT/agents/`',
653
- );
654
- }
655
- text = text.replace(
656
- /(^|[\s`("'])\/ultra-(?!builder-pro:)([a-z][a-z0-9-]*)/gm,
657
- '$1/ultra-builder-pro:ultra-$2',
658
- );
659
- text = text.replace(
660
- /(^|[\s`("'])\/clear(?=$|[\s`),.;:])/gm,
661
- '$1/new',
662
- );
663
- return text;
664
- }
665
-
666
- function transformSkill(buf, relPath, skillName) {
667
- const source = buf.toString('utf8');
668
- if (relPath.endsWith('.json')) {
669
- const transformed = kimiTextTransform(source, skillName);
670
- JSON.parse(transformed);
671
- return Buffer.from(transformed, 'utf8');
672
- }
673
- if (!relPath.endsWith('.md')) return buf;
674
- const { fm, body } = parseFm(source);
675
- if (!fm) return Buffer.from(kimiTextTransform(source, skillName), 'utf8');
676
- const policy = skillPolicy(skillName);
677
- const nativeFm = {
678
- name: String(fm.name || skillName),
679
- description: kimiTextTransform(fm.description || `${skillName} workflow`, skillName)
680
- .replace(/\s+/g, ' ')
681
- .trim(),
682
- ...(policy.userInvocable ? { disableModelInvocation: true } : {}),
683
- };
684
- return Buffer.from(serializeFm(nativeFm, kimiTextTransform(body, skillName)), 'utf8');
685
- }
686
-
687
- function transformAgent(buf, relPath) {
688
- if (!relPath.endsWith('.md')) return buf;
689
- const source = buf.toString('utf8');
690
- const { fm, body } = parseFm(source);
691
- const name = path.basename(relPath, '.md');
692
- if (!fm) return Buffer.from(kimiTextTransform(source, name), 'utf8');
693
- return Buffer.from(serializeFm({
694
- name: String(fm.name || name),
695
- description: kimiTextTransform(fm.description || `${name} worker`, name)
696
- .replace(/\s+/g, ' ')
697
- .trim(),
698
- }, kimiTextTransform(body, name)), 'utf8');
699
- }
700
-
701
- function copySkills(repoRoot, pluginRoot) {
702
- const installed = [];
703
- for (const name of skillsForRuntime('kimi')) {
704
- const source = path.join(repoRoot, 'skills', name);
705
- if (!fs.existsSync(path.join(source, 'SKILL.md'))) {
706
- throw new Error(`missing allowlisted Kimi skill: ${name}`);
707
- }
708
- copyTree(source, path.join(pluginRoot, 'skills', name), {
709
- transform: (buf, relPath) => transformSkill(buf, relPath, name),
710
- });
711
- installed.push(name);
712
- }
713
- return installed;
714
- }
715
-
716
- function commandDescription(repoRoot, name) {
717
- const source = path.join(repoRoot, 'commands', `${name}.md`);
718
- if (fs.existsSync(source)) {
719
- const { fm } = parseFm(fs.readFileSync(source, 'utf8'));
720
- if (fm && typeof fm.description === 'string' && fm.description.trim()) {
721
- return kimiTextTransform(fm.description, name).replace(/\s+/g, ' ').trim();
722
- }
723
- }
724
- return `Run the ${name} Ultra Builder Pro workflow.`;
725
- }
726
-
727
- function copyCommands(repoRoot, pluginRoot) {
728
- const root = path.join(pluginRoot, 'commands');
729
- ensureDir(root);
730
- for (const name of CORE_PUBLIC_SKILLS) {
731
- writeAtomic(path.join(root, `${name}.md`), `---
732
- description: ${JSON.stringify(commandDescription(repoRoot, name))}
733
- ---
734
-
735
- Use the registered \`${name}\` skill now and follow its complete workflow. Treat the current Kimi
736
- Code session as the primary host; use native Kimi tools and the bundled Ultra MCP. Do not mutate
737
- generated Ultra projections directly.
738
-
739
- Arguments: $ARGUMENTS
740
- `);
741
- }
742
- return CORE_PUBLIC_SKILLS.map((name) => `${name}.md`);
743
- }
744
-
745
- function copyAgents(repoRoot, pluginRoot) {
746
- return copyTree(path.join(repoRoot, 'agents'), path.join(pluginRoot, 'agents'), {
747
- transform: transformAgent,
748
- });
749
- }
750
-
751
- function hookCommand(feature, ...args) {
752
- return [
753
- 'python3 ./hooks/adapters/kimi.py',
754
- JSON.stringify(feature),
755
- ...args.map((arg) => JSON.stringify(arg)),
756
- ].join(' ');
51
+ function hookCommand(name) {
52
+ return `python3 ./hooks/adapters/kimi.py ${JSON.stringify(name)}`;
757
53
  }
758
54
 
759
55
  function buildHooksManifest() {
760
56
  return [
761
- { event: 'SessionStart', command: hookCommand('health_check.py'), timeout: 5 },
762
- { event: 'SessionStart', command: hookCommand('workflow_context.py'), timeout: 10 },
763
- { event: 'PreToolUse', matcher: 'Edit|Write', command: hookCommand('active_task_context.py'), timeout: 5 },
764
- { event: 'PreCompact', command: hookCommand('workflow_checkpoint.py'), timeout: 10 },
765
- { event: 'PostCompact', command: hookCommand('workflow_resume.py'), timeout: 10 },
766
- { event: 'Stop', command: hookCommand('pre_stop_check.py'), timeout: 5 },
767
- { event: 'SubagentStart', command: hookCommand('subagent_tracker.py', 'start'), timeout: 5 },
768
- { event: 'SubagentStop', command: hookCommand('subagent_tracker.py', 'stop'), timeout: 5 },
57
+ { event: 'SessionStart', command: hookCommand('session_context.py'), timeout: 10 },
58
+ { event: 'SessionStart', matcher: 'compact', command: hookCommand('compact_context.py'), timeout: 10 },
59
+ { event: 'PreToolUse', matcher: 'Edit|Write|Grep', command: hookCommand('mid_workflow_recall.py'), timeout: 10 },
60
+ { event: 'PreToolUse', matcher: 'Bash', command: hookCommand('block_dangerous_commands.py'), timeout: 10 },
61
+ { event: 'PostToolUse', matcher: 'Edit|Write', command: hookCommand('post_edit_guard.py'), timeout: 10 },
62
+ { event: 'PreCompact', command: hookCommand('compact_context.py'), timeout: 10 },
63
+ { event: 'PostCompact', command: hookCommand('compact_context.py'), timeout: 10 },
769
64
  ];
770
65
  }
771
66
 
772
- function copyHooks(repoRoot, pluginRoot) {
773
- const root = path.join(pluginRoot, 'hooks');
774
- ensureDir(root);
775
- for (const name of WORKFLOW_HOOK_FILES) {
776
- const source = path.join(repoRoot, 'hooks', name);
777
- if (!fs.existsSync(source)) throw new Error(`missing allowlisted Kimi hook: ${name}`);
778
- fs.copyFileSync(source, path.join(root, name));
779
- }
780
- ensureDir(path.join(root, 'adapters'));
781
- fs.copyFileSync(
782
- path.join(repoRoot, 'hooks', 'adapters', 'kimi.py'),
783
- path.join(root, 'adapters', 'kimi.py'),
784
- );
785
- return [...WORKFLOW_HOOK_FILES, path.join('adapters', 'kimi.py')];
786
- }
787
-
788
- function mcpManifest() {
789
- const enabledTools = [...PUBLIC_TOOLS];
790
- if (process.platform === 'win32') {
791
- return {
792
- transport: 'stdio',
793
- command: 'node.exe',
794
- args: ['./runtime/launch.cjs'],
795
- enabled: true,
796
- enabledTools,
797
- };
798
- }
799
- // Kimi's built-in `node` fallback uses the host binary's ABI. Ultra bundles
800
- // better-sqlite3 for the Node that runs `ubp`, so use PATH Node explicitly.
801
- return {
802
- transport: 'stdio',
803
- command: 'env',
804
- args: ['node', './runtime/launch.cjs'],
805
- enabled: true,
806
- enabledTools,
807
- };
808
- }
809
-
810
- function writeManifest(repoRoot, pluginRoot) {
811
- const pkg = JSON.parse(fs.readFileSync(path.join(repoRoot, 'package.json'), 'utf8'));
812
- const manifest = {
813
- name: PLUGIN_ID,
814
- version: pkg.version,
815
- description: 'Kimi-native Ultra Builder Pro workflows, lifecycle hooks, review workers, and project-local MCP state.',
816
- keywords: ['kimi-code', 'skills', 'hooks', 'mcp', 'ultra-builder-pro'],
817
- author: { name: typeof pkg.author === 'string' ? pkg.author : 'Ultra Builder Pro contributors' },
818
- homepage: pkg.homepage,
819
- license: pkg.license || 'MIT',
820
- skills: ['./skills'],
821
- mcpServers: { [MCP_SERVER_NAME]: mcpManifest() },
822
- hooks: buildHooksManifest(),
823
- commands: ['./commands'],
824
- interface: {
825
- displayName: 'Ultra Builder Pro',
826
- shortDescription: 'Kimi-native engineering workflows and verification gates.',
827
- longDescription: 'Ultra workflows, continuous-change state, review worker templates, lifecycle recovery hooks, and a project-local MCP server adapted to Kimi Code.',
828
- developerName: typeof pkg.author === 'string' ? pkg.author : 'Ultra Builder Pro contributors',
829
- websiteURL: pkg.homepage,
830
- },
831
- skillInstructions: 'Kimi Code remains primary. Use Ultra MCP for durable state, TodoList for session coordination, and Agent or AgentSwarm with bundled prompt templates for bounded workers.',
832
- };
833
- writeAtomic(path.join(pluginRoot, 'kimi.plugin.json'), `${JSON.stringify(manifest, null, 2)}\n`);
834
- return manifest;
835
- }
836
-
837
- function buildStaging(repoRoot, staging) {
838
- const copied = {
839
- commands: copyCommands(repoRoot, staging),
840
- skills: copySkills(repoRoot, staging),
841
- agents: copyAgents(repoRoot, staging),
842
- hooks: copyHooks(repoRoot, staging),
843
- };
844
- buildMcpRuntime(repoRoot, staging, { runtime: 'kimi' });
845
- writeManifest(repoRoot, staging);
846
- if (fs.existsSync(path.join(repoRoot, 'LICENSE'))) {
847
- fs.copyFileSync(path.join(repoRoot, 'LICENSE'), path.join(staging, 'LICENSE'));
67
+ function loadRegistry(file) {
68
+ if (!fs.existsSync(file)) return { version: 1, plugins: [] };
69
+ const value = JSON.parse(fs.readFileSync(file, 'utf8'));
70
+ if (!value || value.version !== 1 || !Array.isArray(value.plugins)) {
71
+ throw new Error(`invalid Kimi registry: ${file}`);
848
72
  }
849
- markManaged(staging, { adapter: 'kimi', plugin: PLUGIN_ID });
850
- return copied;
73
+ return value;
851
74
  }
852
75
 
853
- function upsertRegistry(registry, target, repoRoot) {
854
- const existing = registry.plugins.find((entry) => entry && entry.id === PLUGIN_ID);
76
+ function nextRegistry(value, target, repoRoot) {
77
+ const existing = value.plugins.find((entry) => entry?.id === PLUGIN_ID);
855
78
  if (existing && path.resolve(existing.root || '') !== path.resolve(target)) {
856
79
  throw new Error(`refusing to replace conflicting Kimi registration: ${existing.root}`);
857
80
  }
@@ -864,294 +87,142 @@ function upsertRegistry(registry, target, repoRoot) {
864
87
  installedAt: existing?.installedAt || now,
865
88
  updatedAt: now,
866
89
  originalSource: repoRoot,
867
- ...(existing?.capabilities ? { capabilities: existing.capabilities } : {}),
868
90
  };
869
91
  return {
870
92
  version: 1,
871
- plugins: [...registry.plugins.filter((entry) => !entry || entry.id !== PLUGIN_ID), record],
93
+ plugins: [...value.plugins.filter((entry) => entry?.id !== PLUGIN_ID), record],
872
94
  };
873
95
  }
874
96
 
875
- function writePluginProvenance(repoRoot, target) {
876
- const source = provenance.packageSource(repoRoot);
877
- const file = path.join(target, PROVENANCE_FILE);
878
- const manifest = provenance.writeProvenance({
879
- file,
880
- adapter: 'kimi',
881
- ...source,
882
- roots: { plugin: target },
883
- assets: provenance.assetRefsForTree('plugin', target, {
884
- exclude: ['.ubp-managed', PROVENANCE_FILE],
885
- }),
97
+ function buildStaging(repoRoot, staging, publishedRoot) {
98
+ const skills = copySkills({ runtime: 'kimi', repoRoot, skillRoot: path.join(staging, 'skills') });
99
+ const hooks = buildHooksManifest();
100
+ copyHooks({ runtime: 'kimi', repoRoot, hookRoot: path.join(staging, 'hooks'), hooksManifest: hooks });
101
+ const pkg = JSON.parse(fs.readFileSync(path.join(repoRoot, 'package.json'), 'utf8'));
102
+ writeAtomic(path.join(staging, 'kimi.plugin.json'), `${JSON.stringify({
103
+ name: PLUGIN_ID,
104
+ version: pkg.version,
105
+ description: 'File-first Ultra Builder Pro workflows for Kimi Code.',
106
+ keywords: ['kimi-code', 'skills', 'hooks', 'ultra-builder-pro'],
107
+ author: { name: pkg.author },
108
+ homepage: pkg.homepage,
109
+ license: pkg.license,
110
+ skills: ['./skills'],
111
+ hooks,
112
+ interface: {
113
+ displayName: 'Ultra Builder Pro',
114
+ shortDescription: 'File-first project workflows and evidence.',
115
+ developerName: pkg.author,
116
+ websiteURL: pkg.homepage,
117
+ },
118
+ }, null, 2)}\n`);
119
+ markManaged(staging, { adapter: 'kimi', plugin: PLUGIN_ID, version: pkg.version });
120
+ writePluginProvenance({
121
+ adapter: 'kimi', repoRoot, stagingRoot: staging, publishedRoot,
886
122
  contracts: {
887
123
  plugin_manifest: { root: 'plugin', path: 'kimi.plugin.json' },
888
- mcp_launcher: { root: 'plugin', path: path.join('runtime', 'launch.cjs') },
889
- native_runtime: { root: 'plugin', path: path.join('runtime', 'native-runtime.json') },
890
- context_envelope_helper: { root: 'plugin', path: path.join('runtime', 'hook-context.cjs') },
891
- hook_event_helper: { root: 'plugin', path: path.join('runtime', 'hook-event.cjs') },
892
- hook_adapter: { root: 'plugin', path: path.join('hooks', 'adapters', 'kimi.py') },
124
+ hooks_manifest: { root: 'plugin', path: 'hooks/hooks.json' },
125
+ hook_adapter: { root: 'plugin', path: 'hooks/adapters/kimi.py' },
893
126
  },
894
127
  });
895
- return { file, manifest };
128
+ return { skills };
896
129
  }
897
130
 
898
131
  function install(ctx = {}) {
132
+ assertPluginScope(ctx);
899
133
  const target = resolvePluginRoot(ctx);
134
+ const configRoot = resolveTarget(ctx);
900
135
  const registryFile = resolveRegistryFile(ctx);
901
136
  const repoRoot = resolveRepoRoot(ctx);
902
- const registry = loadRegistry(registryFile);
903
- const existingRecord = registry.plugins.find((entry) => entry && entry.id === PLUGIN_ID);
904
- if (existingRecord && path.resolve(existingRecord.root || '') !== path.resolve(target)) {
905
- throw new Error(`refusing to replace conflicting Kimi registration: ${existingRecord.root}`);
906
- }
907
- if (fs.existsSync(target) && !isManaged(target)) {
908
- throw new Error(`refusing to replace unmanaged Kimi plugin: ${target}`);
909
- }
910
-
911
- const managedDir = path.dirname(target);
912
- ensureDir(managedDir);
913
- const staging = fs.mkdtempSync(path.join(managedDir, `${PLUGIN_ID}-`));
914
- const backup = `${staging}-previous`;
915
- let movedPrevious = false;
916
- let published = false;
137
+ const previousMetadata = managedMetadata(target) || {};
138
+ const cleanupAbsent = previousMetadata.cleanup_absent
139
+ || captureAbsent(configRoot, ['.', 'plugins', path.join('plugins', 'managed')]);
140
+ const registryExisted = fs.existsSync(registryFile);
141
+ const registryCreated = previousMetadata.registry_created === true
142
+ || (previousMetadata.registry_created === undefined && !registryExisted);
143
+ const registryBefore = registryExisted ? fs.readFileSync(registryFile) : null;
144
+ const registry = nextRegistry(loadRegistry(registryFile), target, repoRoot);
145
+ ensureDir(path.dirname(target));
146
+ const staging = fs.mkdtempSync(path.join(path.dirname(target), `.${PLUGIN_ID}-staging-`));
917
147
  try {
918
- const copied = buildStaging(repoRoot, staging);
919
- if (fs.existsSync(target)) {
920
- fs.renameSync(target, backup);
921
- movedPrevious = true;
148
+ const copied = buildStaging(repoRoot, staging, target);
149
+ markManaged(staging, {
150
+ cleanup_absent: cleanupAbsent,
151
+ registry_created: registryCreated,
152
+ });
153
+ writeAtomic(registryFile, `${JSON.stringify(registry, null, 2)}\n`);
154
+ try {
155
+ publishManagedTrees([{ source: staging, target, label: 'Kimi plugin' }]);
156
+ } catch (error) {
157
+ if (registryExisted) writeAtomic(registryFile, registryBefore);
158
+ else if (fs.existsSync(registryFile)) fs.unlinkSync(registryFile);
159
+ throw error;
922
160
  }
923
- fs.renameSync(staging, target);
924
- published = true;
925
- const provenanceReport = writePluginProvenance(repoRoot, target);
926
- writeAtomic(registryFile, `${JSON.stringify(upsertRegistry(registry, target, repoRoot), null, 2)}\n`);
927
- if (movedPrevious) removeTree(backup);
928
161
  return {
929
162
  target,
163
+ pluginRoot: target,
164
+ skillRoot: path.join(target, 'skills'),
165
+ hookRoot: path.join(target, 'hooks'),
930
166
  registry: registryFile,
931
167
  copied,
932
- provenance: provenanceReport,
933
- reload_required: true,
934
168
  };
935
169
  } catch (error) {
936
- if (published && fs.existsSync(target)) removeTree(target);
937
- else if (fs.existsSync(staging)) removeTree(staging);
938
- if (movedPrevious && fs.existsSync(backup)) fs.renameSync(backup, target);
170
+ if (fs.existsSync(staging)) removeTree(staging);
171
+ pruneCreatedEmpty(configRoot, cleanupAbsent);
939
172
  throw error;
940
173
  }
941
174
  }
942
175
 
943
- function addIssue(report, code, details = {}) {
944
- report.issues.push({ code, ...details });
945
- }
946
-
947
176
  function doctor(ctx = {}) {
177
+ assertPluginScope(ctx);
948
178
  const target = resolvePluginRoot(ctx);
949
- const registryFile = resolveRegistryFile(ctx);
950
- const repoRoot = resolveRepoRoot(ctx);
951
- const source = provenance.packageSource(repoRoot);
952
- const report = provenance.inspectProvenance({
953
- file: path.join(target, PROVENANCE_FILE),
954
- expectedAdapter: 'kimi',
955
- expectedPackageVersion: source.packageInfo.version,
179
+ const report = inspectPlugin({
180
+ adapter: 'kimi', repoRoot: resolveRepoRoot(ctx), pluginRoot: target,
181
+ skillRoot: path.join(target, 'skills'), hookRoot: path.join(target, 'hooks'),
182
+ manifestFile: path.join(target, 'kimi.plugin.json'),
956
183
  });
957
-
958
- let registrationOk = false;
959
- try {
960
- const registry = loadRegistry(registryFile);
961
- const record = registry.plugins.find((entry) => entry && entry.id === PLUGIN_ID);
962
- registrationOk = !!record
963
- && path.resolve(record.root || '') === path.resolve(target)
964
- && record.source === 'local-path'
965
- && record.enabled === true;
966
- } catch (error) {
967
- addIssue(report, 'PLUGIN_REGISTRATION_INVALID', { path: registryFile, message: error.message });
968
- }
969
- if (!registrationOk && !report.issues.some((issue) => issue.code === 'PLUGIN_REGISTRATION_INVALID')) {
970
- addIssue(report, 'PLUGIN_REGISTRATION_INVALID', { path: registryFile, expected_root: target });
971
- }
972
-
973
- let manifestOk = false;
974
- let mcpOk = false;
975
- let hooksOk = false;
184
+ let registered = false;
976
185
  try {
977
- const manifest = JSON.parse(fs.readFileSync(path.join(target, 'kimi.plugin.json'), 'utf8'));
978
- manifestOk = manifest.name === PLUGIN_ID
979
- && Array.isArray(manifest.skills)
980
- && manifest.skills.includes('./skills')
981
- && Array.isArray(manifest.commands)
982
- && manifest.commands.includes('./commands')
983
- && manifest.sessionStart === undefined;
984
- const mcp = manifest.mcpServers?.[MCP_SERVER_NAME];
985
- const expected = mcpManifest();
986
- mcpOk = !!mcp
987
- && mcp.transport === expected.transport
988
- && mcp.command === expected.command
989
- && JSON.stringify(mcp.args) === JSON.stringify(expected.args)
990
- && mcp.enabled === true;
991
- hooksOk = Array.isArray(manifest.hooks)
992
- && buildHooksManifest().every((expectedHook) => manifest.hooks.some((hook) => (
993
- hook.event === expectedHook.event
994
- && hook.command === expectedHook.command
995
- && hook.matcher === expectedHook.matcher
996
- )));
997
- } catch (error) {
998
- addIssue(report, 'PLUGIN_MANIFEST_INVALID', {
999
- path: path.join(target, 'kimi.plugin.json'), message: error.message,
1000
- });
1001
- }
1002
- if (!manifestOk && !report.issues.some((issue) => issue.code === 'PLUGIN_MANIFEST_INVALID')) {
1003
- addIssue(report, 'PLUGIN_MANIFEST_INVALID', { path: path.join(target, 'kimi.plugin.json') });
1004
- }
1005
- if (!mcpOk) addIssue(report, 'MCP_REGISTRATION_INVALID', { path: path.join(target, 'kimi.plugin.json') });
1006
- if (!hooksOk) addIssue(report, 'HOOK_REGISTRATION_INVALID', { path: path.join(target, 'kimi.plugin.json') });
1007
-
1008
- report.checks.registration = { status: registrationOk ? 'pass' : 'fail' };
1009
- report.checks.plugin_manifest = { status: manifestOk ? 'pass' : 'fail' };
1010
- report.checks.mcp_registration = { status: mcpOk ? 'pass' : 'fail' };
1011
- report.checks.hook_registration = { status: hooksOk ? 'pass' : 'fail' };
1012
- if (shouldRunHostCli(ctx)) {
1013
- let actual = null;
1014
- try {
1015
- actual = inspectActualKimiRegistration(ctx);
1016
- } catch (error) {
1017
- addIssue(report, 'ACTUAL_REGISTRY_INVALID', {
1018
- path: registryFile,
1019
- message: error.message,
1020
- });
1021
- }
1022
- const actualRegistryOk = !!actual?.registryOk;
1023
- const actualPluginRootOk = actualRegistryOk && actual.pluginRootOk;
1024
- report.checks.actual_registry = { status: actualRegistryOk ? 'pass' : 'fail' };
1025
- report.checks.actual_plugin_root = { status: actualPluginRootOk ? 'pass' : 'fail' };
1026
- if (!actualRegistryOk) {
1027
- addIssue(report, 'ACTUAL_REGISTRY_INVALID', {
1028
- path: registryFile,
1029
- expected_root: target,
1030
- });
1031
- }
1032
- if (!actualPluginRootOk) {
1033
- addIssue(report, 'ACTUAL_PLUGIN_ROOT_INVALID', { path: target });
1034
- }
1035
- if (!actualRegistryOk || !actualPluginRootOk) {
1036
- report.checks.isolated_consumer_cli = { status: 'fail' };
1037
- report.checks.isolated_consumer_plugin = { status: 'fail' };
1038
- report.checks.isolated_consumer_mcp = { status: 'fail' };
1039
- return applyNativeDoctor(report, path.join(target, 'runtime'));
1040
- }
1041
- try {
1042
- const host = inspectKimiHost(ctx);
1043
- report.checks.isolated_consumer_cli = { status: 'pass' };
1044
- report.checks.isolated_consumer_plugin = { status: host.pluginOk ? 'pass' : 'fail' };
1045
- report.checks.isolated_consumer_mcp = { status: host.mcpOk ? 'pass' : 'fail' };
1046
- if (!host.pluginOk) {
1047
- addIssue(report, 'HOST_PLUGIN_NOT_DISCOVERED', { plugin_id: PLUGIN_ID });
1048
- }
1049
- if (!host.mcpOk) {
1050
- addIssue(report, 'HOST_MCP_NOT_DISCOVERED', {
1051
- plugin_id: PLUGIN_ID,
1052
- server: MCP_SERVER_NAME,
1053
- });
1054
- }
1055
- } catch (error) {
1056
- report.checks.isolated_consumer_cli = { status: 'fail' };
1057
- report.checks.isolated_consumer_plugin = { status: 'fail' };
1058
- report.checks.isolated_consumer_mcp = { status: 'fail' };
1059
- addIssue(
1060
- report,
1061
- error.code === 'HOST_CLI_TIMEOUT' ? 'HOST_CLI_TIMEOUT' : 'HOST_CLI_DOCTOR_FAILED',
1062
- { message: error.message },
1063
- );
1064
- }
1065
- }
1066
- return applyNativeDoctor(report, path.join(target, 'runtime'));
186
+ const record = loadRegistry(resolveRegistryFile(ctx)).plugins.find((entry) => entry?.id === PLUGIN_ID);
187
+ registered = !!record && record.enabled === true && path.resolve(record.root) === path.resolve(target);
188
+ } catch {}
189
+ report.checks.registration = { status: registered ? 'pass' : 'fail' };
190
+ if (!registered) report.issues.push({ code: 'PLUGIN_REGISTRATION_INVALID' });
191
+ report.status = report.issues.length === 0 ? 'healthy' : 'degraded';
192
+ return report;
1067
193
  }
1068
194
 
1069
195
  function uninstall(ctx = {}) {
196
+ assertPluginScope(ctx);
1070
197
  const target = resolvePluginRoot(ctx);
198
+ const configRoot = resolveTarget(ctx);
199
+ const metadata = managedMetadata(target) || {};
200
+ const cleanupAbsent = metadata.cleanup_absent || [];
1071
201
  const registryFile = resolveRegistryFile(ctx);
1072
- const registryExisted = fs.existsSync(registryFile);
1073
- const registryBytes = registryExisted ? fs.readFileSync(registryFile) : null;
1074
202
  const registry = loadRegistry(registryFile);
1075
- const record = registry.plugins.find((entry) => entry && entry.id === PLUGIN_ID);
203
+ const record = registry.plugins.find((entry) => entry?.id === PLUGIN_ID);
1076
204
  if (record && path.resolve(record.root || '') !== path.resolve(target)) {
1077
205
  throw new Error(`refusing to remove conflicting Kimi registration: ${record.root}`);
1078
206
  }
1079
- if (fs.existsSync(target) && !isManaged(target)) {
1080
- throw new Error(`refusing to remove unmanaged Kimi plugin: ${target}`);
1081
- }
1082
-
1083
- const report = { target, registry: registryFile, removed: {} };
1084
- const backup = path.join(
1085
- path.dirname(target),
1086
- `.${PLUGIN_ID}.uninstall-backup-${crypto.randomUUID()}`,
1087
- );
1088
- let pluginMoved = false;
1089
- let registryPublicationAttempted = false;
1090
- try {
1091
- if (fs.existsSync(target)) {
1092
- fs.renameSync(target, backup);
1093
- pluginMoved = true;
1094
- }
1095
- if (record) {
1096
- const expectedBytes = Buffer.from(`${JSON.stringify({
1097
- version: 1,
1098
- plugins: registry.plugins.filter((entry) => !entry || entry.id !== PLUGIN_ID),
1099
- }, null, 2)}\n`);
1100
- registryPublicationAttempted = true;
1101
- writeAtomic(registryFile, expectedBytes);
1102
- const readback = fs.readFileSync(registryFile);
1103
- if (!readback.equals(expectedBytes)) {
1104
- throw new Error(`Kimi registry readback mismatch after uninstall: ${registryFile}`);
1105
- }
1106
- const verified = loadRegistry(registryFile);
1107
- if (verified.plugins.some((entry) => entry && entry.id === PLUGIN_ID)) {
1108
- throw new Error(`Kimi registry still contains ${PLUGIN_ID} after uninstall`);
1109
- }
1110
- report.removed.registration = true;
1111
- }
1112
- if (pluginMoved) {
1113
- removeTree(backup);
1114
- report.removed.plugin = true;
1115
- }
1116
- return report;
1117
- } catch (error) {
1118
- const rollbackErrors = [];
1119
- if (registryPublicationAttempted) {
1120
- try {
1121
- if (registryExisted) {
1122
- writeAtomic(registryFile, registryBytes);
1123
- } else if (fs.existsSync(registryFile)) {
1124
- fs.unlinkSync(registryFile);
1125
- }
1126
- } catch (rollbackError) {
1127
- rollbackErrors.push(rollbackError);
1128
- }
1129
- }
1130
- if (pluginMoved && fs.existsSync(backup)) {
1131
- try {
1132
- if (fs.existsSync(target)) removeTree(target);
1133
- fs.renameSync(backup, target);
1134
- } catch (rollbackError) {
1135
- rollbackErrors.push(rollbackError);
1136
- }
1137
- }
1138
- if (rollbackErrors.length > 0) {
1139
- throw new AggregateError(
1140
- [error, ...rollbackErrors],
1141
- 'Kimi uninstall and rollback both failed',
1142
- );
1143
- }
1144
- throw error;
207
+ if (record) {
208
+ registry.plugins = registry.plugins.filter((entry) => entry?.id !== PLUGIN_ID);
209
+ if (registry.plugins.length === 0 && metadata.registry_created === true) fs.unlinkSync(registryFile);
210
+ else writeAtomic(registryFile, `${JSON.stringify(registry, null, 2)}\n`);
1145
211
  }
212
+ const removedPlugin = removeManaged(target, 'Kimi plugin');
213
+ const cleaned = pruneCreatedEmpty(configRoot, cleanupAbsent);
214
+ return {
215
+ target,
216
+ pluginRoot: target,
217
+ removed: { plugin: removedPlugin, registration: !!record },
218
+ cleaned,
219
+ };
1146
220
  }
1147
221
 
1148
222
  module.exports = {
1149
223
  name: 'kimi',
1150
224
  PLUGIN_ID,
1151
- MCP_SERVER_NAME,
1152
225
  buildHooksManifest,
1153
- inspectKimiHost,
1154
- kimiTextTransform,
1155
226
  resolveTarget,
1156
227
  resolvePluginRoot,
1157
228
  resolveRegistryFile,