ultra-builder-pro-cli 0.25.1 → 0.26.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.ultra-template/changes/abandoned/.gitkeep +0 -0
- package/.ultra-template/contexts/TEMPLATE.md +94 -0
- package/.ultra-template/decisions/.gitkeep +1 -0
- package/.ultra-template/evidence/.gitkeep +1 -0
- package/.ultra-template/north-star.md +46 -0
- package/.ultra-template/research/.gitkeep +1 -0
- package/.ultra-template/specs/discovery.md +2 -2
- package/.ultra-template/tasks.json +3 -0
- package/.ultra-template/test-report.json +27 -0
- package/CHANGELOG.md +107 -1
- package/README.md +217 -579
- package/adapters/_shared/delegate-profiles/kimi-read-only.md +15 -0
- package/adapters/_shared/delegate-profiles/kimi-write.md +17 -0
- package/adapters/_shared/file-ops.cjs +76 -2
- package/adapters/_shared/host-profile.cjs +97 -0
- package/adapters/_shared/plugin-core.cjs +204 -0
- package/adapters/_shared/runtime-assets.cjs +31 -96
- package/adapters/claude.js +92 -291
- package/adapters/codex.js +309 -842
- package/adapters/grok.js +143 -1469
- package/adapters/kimi.js +124 -1053
- package/adapters/opencode.js +230 -742
- package/bin/delegate-worker.cjs +328 -0
- package/bin/delegate.cjs +463 -0
- package/bin/install.js +38 -11
- package/docs/ARCHITECTURE.md +125 -216
- package/docs/ARTIFACT-AUTHORITY.md +90 -268
- package/docs/DECISIONS.md +77 -87
- package/docs/PHILOSOPHY.md +208 -0
- package/docs/PLUGIN-ISOLATION-CONTRACT.md +65 -39
- package/docs/RUNTIME-COMPAT-MATRIX.md +140 -155
- package/docs/SKILL-AUTHORING.md +94 -73
- package/docs/WORKFLOW-LIFECYCLE.md +97 -189
- package/hooks/README.md +19 -37
- package/hooks/_common.py +113 -0
- package/hooks/adapters/codex.py +9 -16
- package/hooks/adapters/grok.py +5 -5
- package/hooks/adapters/kimi.py +8 -14
- package/hooks/block_dangerous_commands.py +201 -0
- package/hooks/compact_context.py +65 -0
- package/hooks/mid_workflow_recall.py +25 -0
- package/hooks/post_edit_guard.py +105 -0
- package/hooks/session_context.py +30 -0
- package/package.json +17 -52
- package/skills/ultra-change/SKILL.md +115 -50
- package/skills/ultra-change/references/change-contract.md +68 -3
- package/skills/ultra-delegate/SKILL.md +89 -0
- package/skills/ultra-delegate/references/delegation-contract.md +59 -0
- package/skills/ultra-delegate/scripts/delegate_wait.py +86 -0
- package/skills/ultra-deliver/SKILL.md +98 -42
- package/skills/ultra-deliver/references/baseline-reconciliation.md +39 -20
- package/skills/ultra-dev/SKILL.md +115 -62
- package/skills/ultra-dev/references/debugging.md +22 -0
- package/skills/ultra-domain-modeling/SKILL.md +79 -0
- package/skills/ultra-grilling/SKILL.md +118 -0
- package/skills/ultra-grilling/references/reframing.md +72 -0
- package/skills/ultra-init/SKILL.md +102 -37
- package/skills/ultra-init/references/brownfield-adoption.md +33 -83
- package/skills/ultra-init/scripts/init_project.cjs +84 -0
- package/skills/ultra-plan/SKILL.md +101 -68
- package/skills/ultra-research/SKILL.md +68 -48
- package/skills/ultra-research/references/00-problem-validation.md +5 -5
- package/skills/ultra-research/references/01-opportunity-discovery.md +3 -3
- package/skills/ultra-research/references/02-market-assessment.md +5 -4
- package/skills/ultra-research/references/03-competitive-landscape.md +3 -3
- package/skills/ultra-research/references/04-product-strategy.md +3 -3
- package/skills/ultra-research/references/05-assumptions-validation.md +5 -5
- package/skills/ultra-research/references/10-user-personas.md +3 -2
- package/skills/ultra-research/references/11-user-scenarios.md +3 -3
- package/skills/ultra-research/references/20-user-stories.md +4 -3
- package/skills/ultra-research/references/21-features-scope.md +3 -2
- package/skills/ultra-research/references/22-success-metrics.md +3 -3
- package/skills/ultra-research/references/30-architecture-context.md +3 -3
- package/skills/ultra-research/references/31-solution-strategy.md +3 -3
- package/skills/ultra-research/references/32-building-blocks.md +3 -3
- package/skills/ultra-research/references/40-deployment.md +3 -3
- package/skills/ultra-research/references/41-quality-risks.md +3 -3
- package/skills/ultra-research/references/99-synthesis.md +7 -7
- package/skills/ultra-review/SKILL.md +84 -32
- package/skills/ultra-review/references/code.md +17 -0
- package/skills/ultra-review/references/comments.md +12 -0
- package/skills/ultra-review/references/design.md +12 -0
- package/skills/ultra-review/references/errors.md +13 -0
- package/skills/ultra-review/references/spec.md +13 -0
- package/skills/ultra-review/references/tests.md +12 -0
- package/skills/ultra-review/references/worker-packet.md +40 -0
- package/skills/ultra-review/scripts/review_wait.py +26 -15
- package/skills/ultra-status/SKILL.md +61 -36
- package/skills/ultra-tdd/SKILL.md +81 -0
- package/skills/ultra-tdd/references/templates/README.md +20 -0
- package/skills/ultra-tdd/references/templates/feature-flag-default-audit.sh +79 -0
- package/skills/ultra-tdd/references/templates/persistence-real.ts +78 -0
- package/skills/ultra-tdd/references/templates/testcontainer-postgres.py +78 -0
- package/skills/ultra-tdd/references/templates/testcontainer-postgres.ts +73 -0
- package/skills/ultra-tdd/references/templates/vertical-slice.ts +66 -0
- package/skills/ultra-tdd/references/test-execution.md +18 -0
- package/skills/ultra-test/SKILL.md +75 -30
- package/skills/ultra-test/references/export-syntax.md +18 -0
- package/skills/ultra-test/scripts/worktree_digest.cjs +54 -0
- package/skills/ultra-think/SKILL.md +49 -27
- package/skills/ultra-think/references/autonomy-boundary.md +48 -0
- package/skills/ultra-think/references/interaction-boundary.md +18 -33
- package/.ultra-template/docs/research/README.md +0 -55
- package/.ultra-template/reports/templates/delivery-report.json +0 -17
- package/.ultra-template/reports/templates/test-report.json +0 -18
- package/.ultra-template/tasks/tasks.json +0 -15
- package/.ultra-template/templates/task-context.md +0 -19
- package/adapters/_shared/codex-assets.cjs +0 -725
- package/adapters/_shared/codex-ultra-tools-entry.cjs +0 -11
- package/adapters/_shared/interaction-contract.cjs +0 -129
- package/adapters/_shared/path-rewrite.cjs +0 -79
- package/adapters/_shared/runtime-native.cjs +0 -132
- package/adapters/_shared/settings-merge.cjs +0 -76
- package/agents/code-reviewer.md +0 -42
- package/agents/debugger.md +0 -40
- package/agents/review-code.md +0 -49
- package/agents/review-comments.md +0 -38
- package/agents/review-coordinator.md +0 -56
- package/agents/review-design.md +0 -40
- package/agents/review-errors.md +0 -43
- package/agents/review-spec.md +0 -39
- package/agents/review-tests.md +0 -40
- package/agents/tdd-runner.md +0 -38
- package/bin/orchestrator.js +0 -383
- package/commands/ultra-change.md +0 -10
- package/commands/ultra-deliver.md +0 -10
- package/commands/ultra-dev.md +0 -10
- package/commands/ultra-doctor.md +0 -10
- package/commands/ultra-init.md +0 -10
- package/commands/ultra-plan.md +0 -10
- package/commands/ultra-research.md +0 -10
- package/commands/ultra-review.md +0 -10
- package/commands/ultra-status.md +0 -10
- package/commands/ultra-test.md +0 -10
- package/commands/ultra-think.md +0 -10
- package/docs/AGENT-CONTEXT.md +0 -191
- package/docs/COMMIT-HASH-BACKFILL.md +0 -51
- package/docs/LEGACY-CLI-CROSSWALK.md +0 -175
- package/docs/ROADMAP.md +0 -101
- package/docs/STATE-DB-ACCESS-POLICY.md +0 -161
- package/hooks/active_task_context.py +0 -152
- package/hooks/context_envelope.py +0 -100
- package/hooks/health_check.py +0 -196
- package/hooks/pre_stop_check.py +0 -66
- package/hooks/runtime_paths.py +0 -396
- package/hooks/subagent_tracker.py +0 -88
- package/hooks/workflow_checkpoint.py +0 -76
- package/hooks/workflow_context.py +0 -48
- package/hooks/workflow_resume.py +0 -84
- package/mcp-server/hook-context.cjs +0 -38
- package/mcp-server/hook-event.cjs +0 -34
- package/mcp-server/lib/archive-journal.cjs +0 -1119
- package/mcp-server/lib/archive-mutation-worker.py +0 -365
- package/mcp-server/lib/artifact-registry.cjs +0 -1232
- package/mcp-server/lib/baseline-workflow.cjs +0 -1193
- package/mcp-server/lib/canonical-json.cjs +0 -19
- package/mcp-server/lib/change-authority.cjs +0 -26
- package/mcp-server/lib/change-packet.cjs +0 -790
- package/mcp-server/lib/change-workflow.cjs +0 -65
- package/mcp-server/lib/context-envelope.cjs +0 -581
- package/mcp-server/lib/context-paths.cjs +0 -142
- package/mcp-server/lib/decision-records.cjs +0 -423
- package/mcp-server/lib/delivery-transaction.cjs +0 -471
- package/mcp-server/lib/doctor-backup-worker.cjs +0 -328
- package/mcp-server/lib/doctor.cjs +0 -627
- package/mcp-server/lib/git-bootstrap.cjs +0 -651
- package/mcp-server/lib/init-project.cjs +0 -1021
- package/mcp-server/lib/kernel-change-workflow.cjs +0 -2029
- package/mcp-server/lib/legacy-change-workflow.cjs +0 -3032
- package/mcp-server/lib/managed-file-write.cjs +0 -172
- package/mcp-server/lib/plan-checkpoint.cjs +0 -177
- package/mcp-server/lib/plan-publication-worker.cjs +0 -830
- package/mcp-server/lib/plan-store.cjs +0 -794
- package/mcp-server/lib/prd-parser.cjs +0 -83
- package/mcp-server/lib/pricing.cjs +0 -44
- package/mcp-server/lib/project-context.cjs +0 -178
- package/mcp-server/lib/projector.cjs +0 -386
- package/mcp-server/lib/provider-refs.cjs +0 -42
- package/mcp-server/lib/runtime-paths.cjs +0 -1714
- package/mcp-server/lib/runtime-state.cjs +0 -222
- package/mcp-server/lib/safe-project-file.cjs +0 -385
- package/mcp-server/lib/stage-checkpoints.cjs +0 -463
- package/mcp-server/lib/state-authority.cjs +0 -95
- package/mcp-server/lib/state-db.cjs +0 -1580
- package/mcp-server/lib/state-ops.cjs +0 -1428
- package/mcp-server/lib/task-contract.cjs +0 -404
- package/mcp-server/lib/task-expander.cjs +0 -105
- package/mcp-server/lib/task-ledger.cjs +0 -1964
- package/mcp-server/lib/telemetry.cjs +0 -85
- package/mcp-server/lib/topo.cjs +0 -131
- package/mcp-server/lib/ultra-facade.cjs +0 -2684
- package/mcp-server/lib/worker-packet.cjs +0 -358
- package/mcp-server/server.cjs +0 -437
- package/orchestrator/auto-merge.cjs +0 -128
- package/orchestrator/daemon.cjs +0 -264
- package/orchestrator/dispatch-rules.cjs +0 -78
- package/orchestrator/parallel-orchestrator.cjs +0 -903
- package/orchestrator/planner/plan-builder.cjs +0 -125
- package/orchestrator/recovery.cjs +0 -188
- package/orchestrator/session-close-journal-worker.cjs +0 -5
- package/orchestrator/session-close-journal.cjs +0 -1014
- package/orchestrator/session-runner.cjs +0 -1153
- package/orchestrator/worktree-manager.cjs +0 -282
- package/settings.json +0 -7
- package/skills/cc-collab/SKILL.md +0 -42
- package/skills/code-review-expert/SKILL.md +0 -43
- package/skills/code-review-expert/references/correctness-reliability.md +0 -30
- package/skills/code-review-expert/references/design-boundaries.md +0 -26
- package/skills/code-review-expert/references/removal-plan.md +0 -24
- package/skills/codex-collab/SKILL.md +0 -49
- package/skills/codex-collab/evals/evals.json +0 -35
- package/skills/integration-rules/SKILL.md +0 -32
- package/skills/security-rules/SKILL.md +0 -38
- package/skills/testing-rules/SKILL.md +0 -32
- package/skills/ultra-doctor/SKILL.md +0 -41
- package/skills/ultra-plan/references/semantic-preflight.md +0 -20
- package/skills/ultra-verify/SKILL.md +0 -46
- package/skills/ultra-verify/evals/evals.json +0 -46
- package/skills/ultra-verify/references/cross-verify-modes.md +0 -28
- package/skills/ultra-verify/references/evidence-status.md +0 -21
- package/skills/ultra-verify/scripts/verify_wait.py +0 -94
- package/spec/cli-protocol.md +0 -102
- package/spec/mcp-tools.yaml +0 -312
- package/spec/schemas/baseline-reconciliation.v1.schema.json +0 -73
- package/spec/schemas/change-delta.v1.schema.json +0 -153
- package/spec/schemas/command-manifest.schema.json +0 -29
- package/spec/schemas/context-file.v4.5.schema.json +0 -54
- package/spec/schemas/delivery-report.v1.schema.json +0 -66
- package/spec/schemas/documentation-reconciliation.v1.schema.json +0 -106
- package/spec/schemas/mcp-tools.schema.json +0 -136
- package/spec/schemas/skill-manifest.schema.json +0 -24
- package/spec/schemas/state-db.sql +0 -641
- package/spec/schemas/task-ledger.v2.schema.json +0 -296
- package/spec/schemas/tasks.v4.5.schema.json +0 -96
- package/spec/schemas/test-report.v1.schema.json +0 -225
- package/ultra-tools/cli.cjs +0 -129
- package/ultra-tools/commands/db.cjs +0 -226
- package/ultra-tools/commands/legacy-memory.cjs +0 -159
- package/ultra-tools/commands/migrate.cjs +0 -1386
- package/ultra-tools/commands/session.cjs +0 -213
- package/ultra-tools/commands/status.cjs +0 -559
- package/ultra-tools/commands/system.cjs +0 -498
- 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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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),
|
|
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),
|
|
38
|
+
return path.join(resolveTarget(ctx), 'plugins', 'installed.json');
|
|
310
39
|
}
|
|
311
40
|
|
|
312
41
|
function resolveRepoRoot(ctx = {}) {
|
|
313
|
-
return
|
|
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
|
|
392
|
-
|
|
393
|
-
|
|
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
|
|
401
|
-
return
|
|
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('
|
|
762
|
-
{ event: 'SessionStart', command: hookCommand('
|
|
763
|
-
{ event: 'PreToolUse', matcher: 'Edit|Write', command: hookCommand('
|
|
764
|
-
{ event: '
|
|
765
|
-
{ event: '
|
|
766
|
-
{ event: '
|
|
767
|
-
{ event: '
|
|
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
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
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
|
-
|
|
850
|
-
return copied;
|
|
73
|
+
return value;
|
|
851
74
|
}
|
|
852
75
|
|
|
853
|
-
function
|
|
854
|
-
const existing =
|
|
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: [...
|
|
93
|
+
plugins: [...value.plugins.filter((entry) => entry?.id !== PLUGIN_ID), record],
|
|
872
94
|
};
|
|
873
95
|
}
|
|
874
96
|
|
|
875
|
-
function
|
|
876
|
-
const
|
|
877
|
-
const
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
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
|
-
|
|
889
|
-
|
|
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 {
|
|
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
|
|
903
|
-
const
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
const
|
|
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
|
-
|
|
920
|
-
|
|
921
|
-
|
|
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 (
|
|
937
|
-
|
|
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
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
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
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
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
|
|
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 (
|
|
1080
|
-
|
|
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,
|