Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/report-discovered-plugin-skills.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

Report discovered plugin skills in plugin manager summaries.
25 changes: 20 additions & 5 deletions packages/agent-core/src/plugin/manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -298,7 +298,7 @@ async function copyPluginToManagedRoot(
return realpath(managedRoot);
}

function recordFrom(input: {
async function recordFrom(input: {
id: string;
root: string;
enabled: boolean;
Expand All @@ -308,7 +308,7 @@ function recordFrom(input: {
capabilities?: PluginCapabilityState;
source?: PluginSource;
parsed: ParsedManifestResult;
}): PluginRecord {
}): Promise<PluginRecord> {
const { parsed } = input;
const hasError = parsed.diagnostics.some((d) => d.severity === 'error');
return {
Expand All @@ -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,
Expand All @@ -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<number> {
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),
Expand Down
1 change: 1 addition & 0 deletions packages/agent-core/src/plugin/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
36 changes: 29 additions & 7 deletions packages/agent-core/test/plugin/manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ async function makePlugin(
name: string,
options: {
skills?: boolean;
skillNames?: readonly string[];
version?: string;
sessionStartSkill?: string;
mcpServers?: Record<string, unknown>;
Expand All @@ -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 };
Expand Down Expand Up @@ -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');
Expand Down
Loading