Download packages/agent-core-v2/src/app/plugin/manager.ts from SaylorTwift/kimi-code: direct link, hf CLI and curl.
- Browser
- Download file 25.6 kB
-
https://huggingface.co/SaylorTwift/kimi-code/resolve/main/packages/agent-core-v2/src/app/plugin/manager.ts
- Command line
-
hf download hf://SaylorTwift/kimi-code/packages/agent-core-v2/src/app/plugin/manager.ts
-
curl -L -o manager.ts https://huggingface.co/SaylorTwift/kimi-code/resolve/main/packages/agent-core-v2/src/app/plugin/manager.ts
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; | |
| } | |