import { KIMI_CODE_PROVIDER_NAME } from '@moonshot-ai/kimi-code-oauth'; import { ScopeActivation, registerScopedService } from '#/_base/di/scope'; import { Service } from '#/_base/di/service'; import { AsyncEmitter, Emitter, type Event } from '#/_base/event'; import type { HookDef } from '#/features/externalHooks/internal/types'; import { IBootstrapService } from '#/app/bootstrap/bootstrap'; import { LifecycleScope } from '#/app/scopes'; import { ISkillDiscovery } from '#/features/skill/catalog/skillDiscovery'; import type { SkillRoot } from '#/features/skill/catalog/types'; import { BugIndicatingError, Error2, PluginErrors } from '#/errors'; import { IProviderService } from '#/llm-adapter/provider/provider'; import type { McpServerConfig } from '#/mcpCore/config-schema'; import { PluginManager } from './manager'; import { type GetPluginInfoInput, type InstallPluginInput, IPluginService, type RemovePluginInput, type SetPluginEnabledInput, type SetPluginMcpServerEnabledInput, } from './plugin'; import type { EnabledPluginSessionStart, EnabledPluginSystemPrompt, PluginCommandDef, PluginInfo, PluginAgentRoot, PluginMcpServerEntry, PluginMutation, PluginMutationSummary, PluginReloadEvent, PluginSummary, PluginUpdateStatus, ReloadSummary, } from './types'; const KIMI_CODE_BASE_URL_ENV = 'KIMI_CODE_BASE_URL'; const KIMI_CODE_OAUTH_HOST_ENV = 'KIMI_CODE_OAUTH_HOST'; const KIMI_OAUTH_HOST_ENV = 'KIMI_OAUTH_HOST'; const NO_ABORT = new AbortController().signal; interface PluginReloadNotification { readonly summary: ReloadSummary; readonly delivery: Promise; } interface PluginMutationOutcome { readonly result: T; readonly notification: PluginReloadNotification; } export class PluginService extends Service implements IPluginService { declare readonly _serviceBrand: undefined; private readonly homeDir: string; private readonly envBaseUrl: string | undefined; private readonly envOAuthHost: string | undefined; private readonly manager: PluginManager; private initialLoadPromise: Promise | undefined; private snapshotLoaded = false; private loadError: Error | undefined; private mutationQueue: Promise = Promise.resolve(); private readonly onDidReloadEmitter = this._register(new AsyncEmitter()); private readonly onDidMutateEmitter = this._register(new Emitter()); readonly onDidReload: Event = this.onDidReloadEmitter.event; readonly onDidMutate: Event = this.onDidMutateEmitter.event; constructor( @IBootstrapService bootstrap: IBootstrapService, @ISkillDiscovery discovery: ISkillDiscovery, @IProviderService private readonly providers: IProviderService, ) { super(); this.homeDir = bootstrap.homeDir; this.envBaseUrl = bootstrap.getEnv(KIMI_CODE_BASE_URL_ENV); this.envOAuthHost = bootstrap.getEnv(KIMI_CODE_OAUTH_HOST_ENV) ?? bootstrap.getEnv(KIMI_OAUTH_HOST_ENV); this.manager = new PluginManager({ kimiHomeDir: this.homeDir, discoverSkills: (roots) => discovery.discover(roots), }); } listPlugins(): Promise { return this.runManagementRead(async () => this.manager.summaries()); } installPlugin(input: InstallPluginInput): Promise { return this.runNotifiedMutation(async () => { const record = await this.manager.install(input.source); const info = this.manager.info(record.id); if (info === undefined) throw new BugIndicatingError(`Plugin "${record.id}" missing right after install`); const notification = await this.reloadAndNotify({ mutation: { kind: 'install', id: record.id }, }); return { result: info, notification }; }); } setPluginEnabled(input: SetPluginEnabledInput): Promise { return this.runNotifiedMutation(async () => { await this.manager.setEnabled(input.id, input.enabled); const notification = await this.reloadAndNotify({ mutation: { kind: input.enabled ? 'enable' : 'disable', id: input.id }, }); return { result: undefined, notification }; }); } setPluginMcpServerEnabled(input: SetPluginMcpServerEnabledInput): Promise { return this.runNotifiedMutation(async () => { await this.manager.setMcpServerEnabled(input.id, input.server, input.enabled); const notification = await this.reloadAndNotify({ mutation: { kind: 'mcp-server', id: input.id }, }); return { result: undefined, notification }; }); } removePlugin(input: RemovePluginInput): Promise { return this.runNotifiedMutation(async () => { await this.manager.remove(input.id); const notification = await this.reloadAndNotify({ mutation: { kind: 'remove', id: input.id }, }); return { result: undefined, notification }; }); } reloadPlugins(): Promise { const reload = this.awaitReloadDelivery( this.enqueueMutation(async () => { try { const notification = await this.reloadAndNotify(); return { result: notification.summary, notification }; } catch (error) { this.loadError = error instanceof Error ? error : new Error(String(error)); throw new Error2( PluginErrors.codes.PLUGIN_LOAD_FAILED, `Failed to reload plugins: ${this.loadError.message}`, { cause: this.loadError, details: { kimiHomeDir: this.homeDir } }, ); } }), ); this.initialLoadPromise ??= reload.then( () => undefined, () => undefined, ); return reload; } private async reloadAndNotify(options?: { readonly mutation: PluginMutation; }): Promise { const summary = await this.manager.reload(); this.snapshotLoaded = true; this.loadError = undefined; const delivery = this.onDidReloadEmitter.fireAsyncConcurrent(summary, NO_ABORT); if (options?.mutation !== undefined) this.onDidMutateEmitter.fire({ ...summary, mutation: options.mutation }); return { summary, delivery }; } private runNotifiedMutation(operation: () => Promise>): Promise { return this.awaitReloadDelivery(this.runSerializedOperation(operation)); } private async awaitReloadDelivery(operation: Promise>): Promise { const { result, notification } = await operation; await notification.delivery; return result; } getPluginInfo(input: GetPluginInfoInput): Promise { return this.runManagementRead(async () => { const info = this.manager.info(input.id); if (info === undefined) { throw new Error2( PluginErrors.codes.PLUGIN_NOT_FOUND, `Plugin "${input.id}" is not installed`, { details: { id: input.id } }, ); } return info; }); } listPluginCommands(): Promise { return this.runSerializedOperation(async () => this.manager.enabledCommands()); } checkUpdates(): Promise { return this.runManagementRead(async () => this.manager.checkUpdates()); } pluginSkillRoots(): Promise { return this.runConsumptionRead([], async () => this.manager.pluginSkillRoots()); } pluginAgentRoots(): Promise { return this.runConsumptionRead([], async () => this.manager.pluginAgentRoots()); } enabledSessionStarts(): Promise { return this.runConsumptionRead([], async () => this.manager.enabledSessionStarts()); } enabledSystemPrompts(): Promise { return this.runConsumptionRead([], async () => this.manager.enabledSystemPrompts()); } enabledMcpServers(): Promise> { return this.runConsumptionRead({}, async () => { const pluginServers = this.manager.enabledMcpServers(); if (!Object.values(pluginServers).some((server) => server.transport === 'stdio')) { return pluginServers; } const managedEnv = await this.managedKimiCodeEnvForPlugins(); return withManagedKimiPluginEnv(pluginServers, managedEnv); }); } mcpServerEntries(): Promise { return this.runManagementRead(async () => { const entries = this.manager.mcpServerEntries(); if (!entries.some((entry) => entry.config.transport === 'stdio')) { return entries; } const managedEnv = await this.managedKimiCodeEnvForPlugins(); return withManagedKimiPluginEnvOnEntries(entries, managedEnv); }); } enabledHooks(): Promise { return this.runConsumptionRead([], async () => this.manager.enabledHooks()); } hasLoadedSnapshot(): boolean { return this.snapshotLoaded; } private runSerializedOperation(operation: () => Promise): Promise { void this.startInitialLoad(); return this.enqueueMutation(async () => { this.assertLoaded(); return operation(); }); } private async runManagementRead(operation: () => Promise): Promise { await this.waitForPendingMutations(); this.assertLoaded(); return operation(); } private async runConsumptionRead(fallback: T, operation: () => Promise): Promise { await this.waitForPendingMutations(); if (!this.snapshotLoaded) return fallback; return operation(); } private async waitForPendingMutations(): Promise { void this.startInitialLoad(); await this.mutationQueue; } private startInitialLoad(): Promise { this.initialLoadPromise ??= this.enqueueMutation(async () => { await this.loadOnce(); }); return this.initialLoadPromise; } private async loadOnce(): Promise { try { await this.manager.load(); this.snapshotLoaded = true; this.loadError = undefined; } catch (error) { this.loadError = error instanceof Error ? error : new Error(String(error)); } } private enqueueMutation(operation: () => Promise): Promise { const result = this.mutationQueue.then(operation); this.mutationQueue = result.then( () => undefined, () => undefined, ); return result; } private assertLoaded(): void { if (this.loadError === undefined) return; throw new Error2( PluginErrors.codes.PLUGIN_LOAD_FAILED, `Plugin state failed to load: ${this.loadError.message}. ` + `Fix the file at ${this.homeDir}/plugins/installed.json and run /plugins reload.`, { cause: this.loadError, details: { kimiHomeDir: this.homeDir } }, ); } private async managedKimiCodeEnvForPlugins(): Promise> { await this.providers.ready; const provider = this.providers.get(KIMI_CODE_PROVIDER_NAME); const envBaseUrl = this.envBaseUrl; const envOAuthHost = this.envOAuthHost; const hasEnvOverride = envBaseUrl !== undefined || envOAuthHost !== undefined; const baseUrl = envBaseUrl !== undefined ? envBaseUrl.replace(/\/+$/, '') : provider?.baseUrl; const oauthHost = hasEnvOverride ? envOAuthHost : provider?.oauth?.oauthHost; const env: Record = {}; if (baseUrl !== undefined) env[KIMI_CODE_BASE_URL_ENV] = baseUrl; if (oauthHost !== undefined) env[KIMI_CODE_OAUTH_HOST_ENV] = oauthHost; return env; } } function withManagedKimiPluginEnv( pluginServers: Record, managedEnv: Record, ): Record { if (Object.keys(managedEnv).length === 0) return pluginServers; const out: Record = {}; for (const [name, server] of Object.entries(pluginServers)) { out[name] = server.transport === 'stdio' ? { ...server, env: { ...server.env, ...managedEnv } } : server; } return out; } function withManagedKimiPluginEnvOnEntries( entries: readonly PluginMcpServerEntry[], managedEnv: Record, ): readonly PluginMcpServerEntry[] { if (Object.keys(managedEnv).length === 0) return entries; return entries.map((entry) => entry.config.transport === 'stdio' ? { ...entry, config: { ...entry.config, env: { ...entry.config.env, ...managedEnv } } } : entry, ); } registerScopedService( LifecycleScope.App, IPluginService, PluginService, ScopeActivation.OnScopeCreated, 'plugin', );