diff --git a/.changeset/report-discovered-plugin-skills.md b/.changeset/report-discovered-plugin-skills.md new file mode 100644 index 0000000000..7b9f9c3762 --- /dev/null +++ b/.changeset/report-discovered-plugin-skills.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Report discovered plugin skills in plugin manager summaries. diff --git a/packages/agent-core/src/plugin/manager.ts b/packages/agent-core/src/plugin/manager.ts index a6ab111668..e42eb7317f 100644 --- a/packages/agent-core/src/plugin/manager.ts +++ b/packages/agent-core/src/plugin/manager.ts @@ -3,7 +3,7 @@ import { tmpdir } from 'node:os'; import path from 'node:path'; import type { McpServerConfig } from '../config/schema'; -import type { SkillRoot } from '../skill'; +import { discoverSkills, type SkillRoot } from '../skill'; import { downloadZip, extractZip } from './archive'; import { parseManifest, type ParsedManifestResult } from './manifest'; import { readInstalled, writeInstalled, type InstalledRecord } from './store'; @@ -103,7 +103,7 @@ export class PluginManager { id = normalizePluginId(parsed.manifest.name); const existing = this.records.get(id); const now = new Date().toISOString(); - const record = recordFrom({ + const record = await recordFrom({ id, root: normalizedRoot, enabled: existing?.enabled ?? true, @@ -298,7 +298,7 @@ async function copyPluginToManagedRoot( return realpath(managedRoot); } -function recordFrom(input: { +async function recordFrom(input: { id: string; root: string; enabled: boolean; @@ -308,7 +308,7 @@ function recordFrom(input: { capabilities?: PluginCapabilityState; source?: PluginSource; parsed: ParsedManifestResult; -}): PluginRecord { +}): Promise { const { parsed } = input; const hasError = parsed.diagnostics.some((d) => d.severity === 'error'); return { @@ -321,6 +321,7 @@ function recordFrom(input: { updatedAt: input.updatedAt, originalSource: input.originalSource, capabilities: input.capabilities, + skillCount: await countDiscoveredPluginSkills(input.id, parsed.manifest), manifest: parsed.manifest, manifestKind: parsed.manifestKind, manifestPath: parsed.manifestPath, @@ -337,13 +338,27 @@ function recordToSummary(record: PluginRecord): PluginSummary { version: record.manifest?.version, enabled: record.enabled, state: record.state, - skillCount: record.manifest?.skills?.length ?? 0, + skillCount: record.skillCount, mcpServerCount: Object.keys(record.manifest?.mcpServers ?? {}).length, enabledMcpServerCount: pluginMcpServersInfo(record).filter((server) => server.enabled).length, hasErrors: record.diagnostics.some((d) => d.severity === 'error'), }; } +async function countDiscoveredPluginSkills( + pluginId: string, + manifest: PluginRecord['manifest'], +): Promise { + const roots = (manifest?.skills ?? []).map((dir) => ({ + path: dir, + source: 'extra', + plugin: { id: pluginId, instructions: manifest?.skillInstructions }, + }) satisfies SkillRoot); + if (roots.length === 0) return 0; + const skills = await discoverSkills({ roots }); + return skills.length; +} + function recordToInfo(record: PluginRecord): PluginInfo { return { ...recordToSummary(record), diff --git a/packages/agent-core/src/plugin/types.ts b/packages/agent-core/src/plugin/types.ts index 34afe8089d..f6d3942ae9 100644 --- a/packages/agent-core/src/plugin/types.ts +++ b/packages/agent-core/src/plugin/types.ts @@ -75,6 +75,7 @@ export interface PluginRecord { readonly originalSource?: string; readonly capabilities?: PluginCapabilityState; readonly skillInstructions?: string; + readonly skillCount: number; readonly manifest?: PluginManifest; readonly manifestKind?: PluginManifestKind; readonly manifestPath?: string; diff --git a/packages/agent-core/test/plugin/manager.test.ts b/packages/agent-core/test/plugin/manager.test.ts index 39dd7a698f..0f3ca03892 100644 --- a/packages/agent-core/test/plugin/manager.test.ts +++ b/packages/agent-core/test/plugin/manager.test.ts @@ -19,6 +19,7 @@ async function makePlugin( name: string, options: { skills?: boolean; + skillNames?: readonly string[]; version?: string; sessionStartSkill?: string; mcpServers?: Record; @@ -29,15 +30,18 @@ async function makePlugin( if (options.version !== undefined) { manifest['version'] = options.version; } - if (options.skills === true) { + const skillNames = options.skillNames ?? (options.skills === true ? ['demo-skill'] : []); + if (skillNames.length > 0) { manifest['skills'] = './skills/'; await mkdir(path.join(root, 'skills'), { recursive: true }); - await mkdir(path.join(root, 'skills', 'demo-skill'), { recursive: true }); - await writeFile( - path.join(root, 'skills', 'demo-skill', 'SKILL.md'), - '---\nname: demo-skill\ndescription: A demo\n---\nbody', - 'utf8', - ); + for (const skillName of skillNames) { + await mkdir(path.join(root, 'skills', skillName), { recursive: true }); + await writeFile( + path.join(root, 'skills', skillName, 'SKILL.md'), + `---\nname: ${skillName}\ndescription: A demo\n---\nbody`, + 'utf8', + ); + } } if (options.sessionStartSkill !== undefined) { manifest['sessionStart'] = { skill: options.sessionStartSkill }; @@ -184,6 +188,24 @@ describe('PluginManager', () => { }); }); + it('summaries count discovered skills inside plugin skill roots', async () => { + const home = await makeKimiHome(); + const root = await makePlugin('superpowers', { + skillNames: ['brainstorming', 'systematic-debugging', 'writing-plans'], + }); + const manager = new PluginManager({ kimiHomeDir: home }); + await manager.load(); + await manager.install(root); + + expect(manager.summaries()).toContainEqual( + expect.objectContaining({ + id: 'superpowers', + skillCount: 3, + }), + ); + expect(manager.info('superpowers')?.skillCount).toBe(3); + }); + it('reload() picks up edits to the managed plugin copy', async () => { const home = await makeKimiHome(); const root = await makePlugin('demo');