SaylorTwift's picture
SaylorTwift HF Staff
Add files using upload-large-folder tool
68d7816 verified
Raw
History Blame Contribute Delete
25.6 kB
import { cp, mkdir, mkdtemp, realpath, rename, rm, stat } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import path from 'node:path';
import type { HookDef } from '#/features/externalHooks/internal/types';
import { discoverFileSkills } from '#/features/skill/catalog/fileSkillDiscovery';
import type { SkillDiscoveryResult } from '#/features/skill/catalog/skillDiscovery';
import type { SkillRoot } from '#/features/skill/catalog/types';
import { BugIndicatingError, Error2, ErrorCodes, PluginErrors } from '#/errors';
import type { McpServerConfig } from '#/mcpCore/config-schema';
import { downloadZip, extractZip } from './archive';
import { loadPluginCommand } from './commands';
import { resolveGithubCommitSha, resolveGithubSource } from './github-resolver';
import { parseManifest, type ParsedManifestResult } from './manifest';
import { resolveInstallSource } from './source';
import { readInstalled, writeInstalled, type InstalledRecord } from './store';
import type { PluginAgentRoot } from './types';
import {
normalizePluginId,
type EnabledPluginSessionStart,
type EnabledPluginSystemPrompt,
type PluginCapabilityState,
type PluginCommandDef,
type PluginGithubMetadata,
type PluginInfo,
type PluginMcpServerEntry,
type PluginMcpServerInfo,
type PluginRecord,
type PluginSource,
type PluginSummary,
type PluginUpdateStatus,
type ReloadSummary,
} from './types';
export interface PluginManagerOptions {
readonly kimiHomeDir: string;
readonly discoverSkills?: (roots: readonly SkillRoot[]) => Promise<SkillDiscoveryResult>;
}
interface ManagedPluginCopy {
readonly root: string;
readonly previousRoot?: string;
}
export class PluginManager {
private readonly kimiHomeDir: string;
private readonly discoverSkills: (roots: readonly SkillRoot[]) => Promise<SkillDiscoveryResult>;
private records = new Map<string, PluginRecord>();
constructor(options: PluginManagerOptions) {
this.kimiHomeDir = options.kimiHomeDir;
this.discoverSkills = options.discoverSkills ?? discoverFileSkills;
}
async load(): Promise<void> {
const file = await readInstalled(this.kimiHomeDir);
const next = new Map<string, PluginRecord>();
for (const entry of file.plugins) {
next.set(entry.id, await this.materialize(entry));
}
this.records = next;
}
list(): readonly PluginRecord[] {
return [...this.records.values()].toSorted((a, b) => a.id.localeCompare(b.id));
}
get(id: string): PluginRecord | undefined {
return this.records.get(normalizePluginId(id));
}
async install(source: string): Promise<PluginRecord> {
const resolved = resolveInstallSource(source);
let sourceRoot: string;
let originalSource: string;
let sourceType: PluginSource;
let zipTmpDir: string | undefined;
let managedCopy: ManagedPluginCopy | undefined;
let github: PluginGithubMetadata | undefined;
try {
if (resolved.kind === 'local-path') {
sourceRoot = await normalizeInstallRoot(resolved.path);
originalSource = resolved.path;
sourceType = 'local-path';
} else {
originalSource = source.trim();
sourceType = resolved.kind === 'github' ? 'github' : 'zip-url';
const zipUrl =
resolved.kind === 'github'
? await (async () => {
const resolution = await resolveGithubSource(resolved);
const installedSha = await installedGithubSha(
resolved.owner,
resolved.repo,
resolution.ref,
);
github = {
owner: resolved.owner,
repo: resolved.repo,
ref: resolution.ref,
installedSha,
};
if (installedSha !== undefined) {
return `https://codeload.github.com/${resolved.owner}/${resolved.repo}/zip/${installedSha}`;
}
return resolution.tarballUrl;
})()
: resolved.path;
const buffer = await downloadZip(zipUrl);
zipTmpDir = await mkdtemp(path.join(tmpdir(), 'kimi-plugin-zip-'));
sourceRoot = await extractZip(buffer, zipTmpDir);
}
const parsed = await parseManifest(sourceRoot);
if (parsed.manifest === undefined) {
const msg =
parsed.diagnostics.find((d) => d.severity === 'error')?.message ?? 'no manifest';
throw new Error2(
ErrorCodes.PLUGIN_LOAD_FAILED,
sourceType === 'local-path'
? `Cannot install plugin at ${sourceRoot}: ${msg}`
: `Cannot install plugin from ${originalSource}: ${msg}`,
{ details: { sourceType } },
);
}
const id = normalizePluginId(parsed.manifest.name);
managedCopy = await copyPluginToManagedRoot(this.kimiHomeDir, id, sourceRoot);
const normalizedRoot = managedCopy.root;
const managedParsed = await parseManifest(normalizedRoot);
const existing = this.records.get(id);
const now = new Date().toISOString();
const record = await recordFrom({
id,
root: normalizedRoot,
enabled: existing?.enabled ?? true,
installedAt: existing?.installedAt ?? now,
updatedAt: now,
originalSource,
source: sourceType,
capabilities: existing?.capabilities,
github,
parsed: managedParsed,
discoverSkills: this.discoverSkills,
});
const next = new Map(this.records);
next.set(id, record);
await this.persist(next);
this.records = next;
if (managedCopy.previousRoot !== undefined) {
await rm(managedCopy.previousRoot, { recursive: true, force: true }).catch(() => undefined);
}
managedCopy = undefined;
return record;
} catch (error) {
if (managedCopy !== undefined) {
try {
await rollbackManagedPluginCopy(managedCopy);
} catch (rollbackError) {
throw new Error2(
ErrorCodes.PLUGIN_LOAD_FAILED,
'Plugin installation failed and the previous managed copy could not be restored',
{
cause: new AggregateError(
[error, rollbackError],
'Plugin installation failed and the previous managed copy could not be restored',
{ cause: error },
),
},
);
}
}
throw error;
} finally {
if (zipTmpDir !== undefined) {
await rm(zipTmpDir, { recursive: true, force: true });
}
}
}
async setEnabled(id: string, enabled: boolean): Promise<void> {
const key = normalizePluginId(id);
const current = this.records.get(key);
if (current === undefined) throw pluginNotFound(id);
if (current.enabled === enabled) return;
const next = new Map(this.records);
next.set(key, { ...current, enabled, updatedAt: new Date().toISOString() });
await this.persist(next);
this.records = next;
}
async setMcpServerEnabled(id: string, server: string, enabled: boolean): Promise<void> {
const key = normalizePluginId(id);
const current = this.records.get(key);
if (current === undefined) throw pluginNotFound(id);
if (current.manifest?.mcpServers?.[server] === undefined) {
throw new Error2(
ErrorCodes.MCP_SERVER_NOT_FOUND,
`Plugin "${id}" does not declare MCP server "${server}"`,
{ details: { id, server } },
);
}
const currentMcpServers = current.capabilities?.mcpServers ?? {};
const nextCapabilities: PluginCapabilityState = {
...current.capabilities,
mcpServers: {
...currentMcpServers,
[server]: { enabled },
},
};
const next = new Map(this.records);
next.set(key, {
...current,
capabilities: nextCapabilities,
updatedAt: new Date().toISOString(),
});
await this.persist(next);
this.records = next;
}
async remove(id: string): Promise<void> {
const key = normalizePluginId(id);
const next = new Map(this.records);
if (!next.delete(key)) {
throw pluginNotFound(id);
}
await this.persist(next);
this.records = next;
}
async checkUpdates(): Promise<readonly PluginUpdateStatus[]> {
const records = [...this.records.values()].filter(
(record) => record.source === 'github' && record.github !== undefined,
);
const results = await Promise.all(
records.map(async (record) => {
try {
return await checkGithubUpdate(record);
} catch {
return undefined;
}
}),
);
return results
.filter((result): result is PluginUpdateStatus => result !== undefined)
.toSorted((a, b) => a.id.localeCompare(b.id));
}
async reload(): Promise<ReloadSummary> {
const prevIds = new Set(this.records.keys());
const file = await readInstalled(this.kimiHomeDir);
const next = new Map<string, PluginRecord>();
const errors: Array<{ id: string; message: string }> = [];
for (const entry of file.plugins) {
try {
next.set(entry.id, await this.materialize(entry));
} catch (error) {
errors.push({ id: entry.id, message: (error as Error).message });
}
}
const added: string[] = [];
for (const id of next.keys()) if (!prevIds.has(id)) added.push(id);
const removed: string[] = [];
for (const id of prevIds) if (!next.has(id)) removed.push(id);
this.records = next;
return { added, removed, errors };
}
enabledHooks(): readonly HookDef[] {
const out: HookDef[] = [];
for (const record of this.records.values()) {
if (!record.enabled || record.state !== 'ok' || record.manifest === undefined) continue;
for (const hook of record.manifest.hooks ?? []) {
out.push({
...hook,
cwd: record.root,
env: {
KIMI_CODE_HOME: this.kimiHomeDir,
KIMI_PLUGIN_ROOT: record.root,
},
});
}
}
return out;
}
async enabledCommands(): Promise<readonly PluginCommandDef[]> {
const out: PluginCommandDef[] = [];
const records = [...this.records.values()];
for (const record of records) {
if (!record.enabled || record.state !== 'ok' || record.manifest === undefined) continue;
for (const entry of record.manifest.commands ?? []) {
const def = await loadPluginCommand({
commandPath: entry.path,
pluginId: record.id,
fallbackName: entry.name,
});
if (def !== undefined) out.push(def);
}
}
return out;
}
pluginSkillRoots(): readonly SkillRoot[] {
const roots: SkillRoot[] = [];
for (const record of this.records.values()) {
if (!record.enabled || record.state !== 'ok' || record.manifest === undefined) continue;
for (const dir of record.manifest.skills ?? []) {
roots.push({
path: dir,
source: 'extra',
plugin: { id: record.id, instructions: record.skillInstructions },
scanMode: record.manifest.rootSkillFallback ? 'root-skill-only' : undefined,
});
}
}
return roots;
}
pluginAgentRoots(): readonly PluginAgentRoot[] {
const roots: PluginAgentRoot[] = [];
for (const record of this.records.values()) {
if (!record.enabled || record.state !== 'ok' || record.manifest === undefined) continue;
for (const dir of record.manifest.agents ?? []) {
roots.push({ path: dir, source: 'plugin' });
}
}
return roots;
}
enabledSessionStarts(): readonly EnabledPluginSessionStart[] {
const out: EnabledPluginSessionStart[] = [];
for (const record of this.records.values()) {
if (!record.enabled || record.state !== 'ok') continue;
const skill = record.manifest?.sessionStart?.skill;
if (skill === undefined) continue;
out.push({ pluginId: record.id, skillName: skill });
}
return out;
}
enabledSystemPrompts(): readonly EnabledPluginSystemPrompt[] {
const out: EnabledPluginSystemPrompt[] = [];
for (const record of this.records.values()) {
if (!record.enabled || record.state !== 'ok') continue;
const content = record.manifest?.systemPrompt;
if (content === undefined) continue;
out.push({ pluginId: record.id, content });
}
return out;
}
enabledMcpServers(): Record<string, McpServerConfig> {
const out: Record<string, McpServerConfig> = {};
for (const record of this.records.values()) {
if (!record.enabled || record.state !== 'ok' || record.manifest === undefined) continue;
for (const [name, config] of Object.entries(record.manifest.mcpServers ?? {})) {
if (!isMcpServerEnabled(record, name, config)) continue;
out[pluginMcpRuntimeName(record.id, name)] = withPluginMcpRuntime(
withMcpServerEnabled(config, true),
record.root,
this.kimiHomeDir,
);
}
}
return out;
}
mcpServerEntries(): readonly PluginMcpServerEntry[] {
const out: PluginMcpServerEntry[] = [];
for (const record of this.records.values()) {
if (record.state !== 'ok' || record.manifest === undefined) continue;
for (const [name, config] of Object.entries(record.manifest.mcpServers ?? {})) {
const enabled = record.enabled && isMcpServerEnabled(record, name, config);
const effective = withPluginMcpRuntime(
withMcpServerEnabled(config, enabled),
record.root,
this.kimiHomeDir,
);
out.push({
name: pluginMcpRuntimeName(record.id, name),
config: effective,
pluginId: record.id,
serverName: name,
});
}
}
return out;
}
summaries(): readonly PluginSummary[] {
return this.list().map((record) => recordToSummary(record));
}
info(id: string): PluginInfo | undefined {
const record = this.get(id);
return record === undefined ? undefined : recordToInfo(record);
}
private async persist(records: ReadonlyMap<string, PluginRecord>): Promise<void> {
const installed: InstalledRecord[] = [...records.values()].map((record) => ({
id: record.id,
root: record.root,
source: record.source,
enabled: record.enabled,
installedAt: record.installedAt,
updatedAt: record.updatedAt,
originalSource: record.originalSource,
capabilities: record.capabilities,
github: record.github,
}));
await writeInstalled(this.kimiHomeDir, { version: 1, plugins: installed });
}
private async materialize(entry: InstalledRecord): Promise<PluginRecord> {
const parsed = await parseManifest(entry.root);
return recordFrom({
id: entry.id,
root: entry.root,
enabled: entry.enabled,
installedAt: entry.installedAt,
updatedAt: entry.updatedAt,
originalSource: entry.originalSource,
capabilities: entry.capabilities,
github: entry.github,
source: entry.source,
parsed,
discoverSkills: this.discoverSkills,
});
}
}
async function installedGithubSha(
owner: string,
repo: string,
ref: PluginGithubMetadata['ref'],
): Promise<string | undefined> {
if (ref.kind === 'sha' && ref.value.length === 40) return ref.value.toLowerCase();
return resolveGithubCommitSha(owner, repo, ref.value);
}
async function checkGithubUpdate(record: PluginRecord): Promise<PluginUpdateStatus> {
const github = record.github;
if (github === undefined)
throw new BugIndicatingError(`Plugin "${record.id}" has no GitHub metadata`);
const current = github.ref;
const pinned = explicitGithubRef(record);
if (pinned?.kind === 'tag' || pinned?.kind === 'sha') {
return {
id: record.id,
source: 'github',
current,
latest: current,
displayVersion: current.value,
updateAvailable: false,
};
}
if (pinned?.kind === 'branch') {
const latestSha = await resolveGithubCommitSha(github.owner, github.repo, pinned.value);
return {
id: record.id,
source: 'github',
current,
latest: current,
displayVersion: latestSha.slice(0, 12),
updateAvailable: github.installedSha === undefined || github.installedSha !== latestSha,
};
}
const latest = await resolveGithubSource({
kind: 'github',
owner: github.owner,
repo: github.repo,
});
let updateAvailable = current.kind !== latest.ref.kind || current.value !== latest.ref.value;
if (!updateAvailable && (latest.ref.kind === 'branch' || latest.ref.kind === 'tag')) {
const latestSha = await resolveGithubCommitSha(github.owner, github.repo, latest.ref.value);
updateAvailable = github.installedSha === undefined || github.installedSha !== latestSha;
}
return {
id: record.id,
source: 'github',
current,
latest: latest.ref,
displayVersion: latest.displayVersion,
updateAvailable,
};
}
function explicitGithubRef(record: PluginRecord): PluginGithubMetadata['ref'] | undefined {
const fallback =
record.github?.ref.kind === 'sha' ||
(record.github?.ref.kind === 'branch' && record.github.ref.value !== 'HEAD')
? record.github.ref
: undefined;
if (record.originalSource === undefined) return fallback;
try {
const source = resolveInstallSource(record.originalSource);
return source.kind === 'github' ? source.ref : fallback;
} catch {
return fallback;
}
}
function pluginNotFound(id: string): Error2 {
return new Error2(PluginErrors.codes.PLUGIN_NOT_FOUND, `Plugin "${id}" is not installed`, {
details: { id },
});
}
async function normalizeInstallRoot(rootPath: string): Promise<string> {
const trimmed = rootPath.trim();
if (!path.isAbsolute(trimmed)) {
throw new Error2(
ErrorCodes.VALIDATION_FAILED,
`Plugin root must be an absolute path (got "${rootPath}")`,
{ details: { path: rootPath } },
);
}
let resolved: string;
try {
resolved = await realpath(trimmed);
} catch (error) {
throw new Error2(ErrorCodes.FS_PATH_NOT_FOUND, `Plugin root does not exist: ${trimmed}`, {
cause: error,
details: { path: trimmed },
});
}
if (!(await stat(resolved)).isDirectory()) {
throw new Error2(ErrorCodes.VALIDATION_FAILED, `Plugin root is not a directory: ${trimmed}`, {
details: { path: trimmed },
});
}
return resolved;
}
async function copyPluginToManagedRoot(
kimiHomeDir: string,
id: string,
sourceRoot: string,
): Promise<ManagedPluginCopy> {
const managedRoot = path.join(kimiHomeDir, 'plugins', 'managed', id);
const managedDir = path.dirname(managedRoot);
await mkdir(managedDir, { recursive: true });
const stagingRoot = await mkdtemp(path.join(managedDir, `${id}-`));
const previousRoot = `${stagingRoot}-previous`;
let movedPreviousRoot = false;
let published = false;
try {
await cp(sourceRoot, stagingRoot, { recursive: true });
try {
await rename(managedRoot, previousRoot);
movedPreviousRoot = true;
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
}
await rename(stagingRoot, managedRoot);
published = true;
return {
root: await realpath(managedRoot),
previousRoot: movedPreviousRoot ? previousRoot : undefined,
};
} catch (error) {
await rm(published ? managedRoot : stagingRoot, { recursive: true, force: true });
if (movedPreviousRoot) await rename(previousRoot, managedRoot);
throw error;
}
}
async function rollbackManagedPluginCopy(copy: ManagedPluginCopy): Promise<void> {
await rm(copy.root, { recursive: true, force: true });
if (copy.previousRoot !== undefined) {
await rename(copy.previousRoot, copy.root);
}
}
async function recordFrom(input: {
id: string;
root: string;
enabled: boolean;
installedAt: string;
updatedAt?: string;
originalSource?: string;
capabilities?: PluginCapabilityState;
github?: PluginGithubMetadata;
source?: PluginSource;
parsed: ParsedManifestResult;
discoverSkills: (roots: readonly SkillRoot[]) => Promise<SkillDiscoveryResult>;
}): Promise<PluginRecord> {
const { parsed } = input;
const hasError = parsed.diagnostics.some((d) => d.severity === 'error');
return {
id: input.id,
root: input.root,
source: input.source ?? 'local-path',
enabled: input.enabled,
state: hasError || parsed.manifest === undefined ? 'error' : 'ok',
installedAt: input.installedAt,
updatedAt: input.updatedAt,
originalSource: input.originalSource,
capabilities: input.capabilities,
github: input.github,
skillCount: await countDiscoveredPluginSkills(input.id, parsed.manifest, input.discoverSkills),
manifest: parsed.manifest,
manifestKind: parsed.manifestKind,
manifestPath: parsed.manifestPath,
shadowedManifestPath: parsed.shadowedManifestPath,
diagnostics: parsed.diagnostics,
skillInstructions: parsed.manifest?.skillInstructions,
};
}
function recordToSummary(record: PluginRecord): PluginSummary {
return {
id: record.id,
displayName: record.manifest?.interface?.displayName ?? record.id,
version: record.manifest?.version,
enabled: record.enabled,
state: record.state,
skillCount: record.skillCount,
mcpServerCount: Object.keys(record.manifest?.mcpServers ?? {}).length,
enabledMcpServerCount: pluginMcpServersInfo(record).filter((server) => server.enabled).length,
hookCount: record.manifest?.hooks?.length ?? 0,
commandCount: record.manifest?.commands?.length ?? 0,
hasErrors: record.diagnostics.some((d) => d.severity === 'error'),
source: record.source,
originalSource: record.originalSource,
github: record.github,
};
}
function recordToInfo(record: PluginRecord): PluginInfo {
return {
...recordToSummary(record),
root: record.root,
installedAt: record.installedAt,
updatedAt: record.updatedAt,
manifestKind: record.manifestKind,
manifestPath: record.manifestPath,
manifest: record.manifest,
mcpServers: pluginMcpServersInfo(record),
shadowedManifestPath: record.shadowedManifestPath,
diagnostics: record.diagnostics,
};
}
function isMcpServerEnabled(record: PluginRecord, name: string, config: McpServerConfig): boolean {
return record.capabilities?.mcpServers?.[name]?.enabled ?? config.enabled !== false;
}
function pluginMcpServersInfo(record: PluginRecord): readonly PluginMcpServerInfo[] {
return Object.entries(record.manifest?.mcpServers ?? {})
.map(([name, config]) => pluginMcpServerInfo(record, name, config))
.toSorted((a, b) => a.name.localeCompare(b.name));
}
function pluginMcpServerInfo(
record: PluginRecord,
name: string,
config: McpServerConfig,
): PluginMcpServerInfo {
if (config.transport === 'http' || config.transport === 'sse') {
return {
name,
runtimeName: pluginMcpRuntimeName(record.id, name),
enabled: isMcpServerEnabled(record, name, config),
transport: config.transport,
url: config.url,
headerKeys: config.headers === undefined ? undefined : Object.keys(config.headers).toSorted(),
};
}
return {
name,
runtimeName: pluginMcpRuntimeName(record.id, name),
enabled: isMcpServerEnabled(record, name, config),
transport: 'stdio',
command: config.command,
args: config.args,
cwd: config.cwd,
envKeys: config.env === undefined ? undefined : Object.keys(config.env).toSorted(),
};
}
function pluginMcpRuntimeName(pluginId: string, serverName: string): string {
return `plugin-${pluginId}:${serverName}`;
}
const KIMI_NODE_FALLBACK_SUBCOMMAND = '__plugin_run_node';
function withMcpServerEnabled(config: McpServerConfig, enabled: boolean): McpServerConfig {
return { ...config, enabled };
}
function withPluginMcpRuntime(
config: McpServerConfig,
pluginRoot: string,
kimiHomeDir: string,
): McpServerConfig {
if (config.transport === 'http' || config.transport === 'sse') return config;
const env = {
...config.env,
KIMI_CODE_HOME: kimiHomeDir,
KIMI_PLUGIN_ROOT: pluginRoot,
};
if (config.command === 'node' && isElectron()) {
return {
...config,
command: process.execPath,
args: config.args ?? [],
cwd: config.cwd ?? pluginRoot,
env: { ...env, ELECTRON_RUN_AS_NODE: '1' },
};
}
if (config.command === 'node' && isKimiNativeBinary()) {
return {
...config,
command: process.execPath,
args: [KIMI_NODE_FALLBACK_SUBCOMMAND, ...(config.args ?? [])],
cwd: config.cwd ?? pluginRoot,
env,
};
}
return { ...config, cwd: config.cwd ?? pluginRoot, env };
}
function isElectron(): boolean {
return typeof process.versions['electron'] === 'string';
}
function isKimiNativeBinary(): boolean {
return !path.basename(process.execPath).toLowerCase().startsWith('node');
}
async function countDiscoveredPluginSkills(
pluginId: string,
manifest: PluginRecord['manifest'],
discoverSkills: (roots: readonly SkillRoot[]) => Promise<SkillDiscoveryResult>,
): Promise<number> {
const dirs = manifest?.skills ?? [];
if (dirs.length === 0) return 0;
const roots: SkillRoot[] = dirs.map((dir) => ({
path: dir,
source: 'extra',
plugin: { id: pluginId, instructions: manifest?.skillInstructions },
scanMode: manifest?.rootSkillFallback ? 'root-skill-only' : undefined,
}));
const result = await discoverSkills(roots);
return result.skills.length;
}