Skip to content
Open
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
37 changes: 32 additions & 5 deletions packages/agent-core/src/config/workspace-local.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const WorkspaceLocalTomlSchema = z.object({
workspace: z
.object({
additional_dir: z.array(z.string()),
plugin_dir: z.array(z.string()).optional(),
})
.optional(),
});
Expand All @@ -22,6 +23,7 @@ export interface WorkspaceAdditionalDirsLoadResult {
readonly projectRoot: string;
readonly configPath: string;
readonly additionalDirs: readonly string[];
readonly pluginDirs: readonly string[];
readonly warning?: string;
}

Expand All @@ -41,14 +43,20 @@ export async function loadWorkspaceLocalConfig(
const file = await readWorkspaceLocalToml(kaos, configPath);

const additionalDirs = file?.parsed.workspace?.additional_dir;
if (additionalDirs === undefined) {
return { projectRoot, configPath, additionalDirs: [] };
const pluginDirs = file?.parsed.workspace?.plugin_dir;
if (additionalDirs === undefined && pluginDirs === undefined) {
return { projectRoot, configPath, additionalDirs: [], pluginDirs: [] };
}

return {
projectRoot,
configPath,
additionalDirs: await resolveAdditionalDirs(kaos, projectRoot, additionalDirs),
additionalDirs: additionalDirs !== undefined
? await resolveAdditionalDirs(kaos, projectRoot, additionalDirs)
: [],
pluginDirs: pluginDirs !== undefined
? await resolvePluginDirs(kaos, projectRoot, pluginDirs)
: [],
};
}

Expand Down Expand Up @@ -81,7 +89,7 @@ export async function appendWorkspaceAdditionalDir(
const fileExistingDirs = resolveExistingAdditionalDirs(kaos, projectRoot, fileAdditionalDirs);

if (hasSameAdditionalDir(kaos, fileExistingDirs, additionalDir)) {
return { projectRoot, configPath, additionalDirs: fileExistingDirs };
return { projectRoot, configPath, additionalDirs: fileExistingDirs, pluginDirs: [] };
}

const workspace = cloneRecord(file.raw['workspace']);
Expand All @@ -91,7 +99,7 @@ export async function appendWorkspaceAdditionalDir(
await kaos.mkdir(dirname(configPath), { parents: true, existOk: true });
await kaos.writeText(configPath, `${stringifyToml(file.raw)}\n`);

return { projectRoot, configPath, additionalDirs: [...fileExistingDirs, additionalDir] };
return { projectRoot, configPath, additionalDirs: [...fileExistingDirs, additionalDir], pluginDirs: [] };
}

export function normalizeAdditionalDirs(additionalDirs: readonly string[]): string[] {
Expand Down Expand Up @@ -182,6 +190,9 @@ function describeWorkspaceLocalValidationError(error: z.ZodError): string {
if (issue?.path[0] === 'workspace' && issue.path[1] === 'additional_dir') {
return 'workspace.additional_dir must be an array of strings';
}
if (issue?.path[0] === 'workspace' && issue.path[1] === 'plugin_dir') {
return 'workspace.plugin_dir must be an array of strings';
}
if (issue?.path[0] === 'workspace') return 'workspace must be a table';
return `Invalid workspace local config: ${error.message}`;
}
Expand All @@ -202,6 +213,22 @@ async function resolveAdditionalDirs(
return resolvedDirs;
}

async function resolvePluginDirs(
kaos: Kaos,
projectRoot: string,
pluginDirs: readonly string[],
): Promise<string[]> {
const resolvedDirs: string[] = [];

for (const pluginDir of normalizeAdditionalDirs(pluginDirs)) {
const resolvedDir = resolvePath(kaos, projectRoot, pluginDir);
if (hasSameAdditionalDir(kaos, resolvedDirs, resolvedDir)) continue;
resolvedDirs.push(resolvedDir);
}

return resolvedDirs;
}

function resolveExistingAdditionalDirs(
kaos: Kaos,
projectRoot: string,
Expand Down
1 change: 1 addition & 0 deletions packages/agent-core/src/plugin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ export type { PluginManagerOptions } from './manager';
export { resolveInstallSource } from './source';
export type { InstallSource, ResolvedSource } from './source';
export { downloadZip, extractZip } from './archive';
export { scanProjectPluginDirs } from './project-plugins';
55 changes: 55 additions & 0 deletions packages/agent-core/src/plugin/project-plugins.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import type { Kaos } from '@moonshot-ai/kaos';
import { join } from 'pathe';

import type { SkillRoot } from '../skill/types';
import type { PluginManifest } from './types';

/**
* Scan project-level plugin directories for plugins.
* Each directory is expected to contain plugin subdirectories with
* `kimi-plugin.json` manifest files.
*/
export async function scanProjectPluginDirs(
kaos: Kaos,
pluginDirs: readonly string[],
): Promise<readonly SkillRoot[]> {
const roots: SkillRoot[] = [];

for (const pluginDir of pluginDirs) {
try {
for await (const entryPath of kaos.iterdir(pluginDir)) {
const manifestPath = join(entryPath, 'kimi-plugin.json');
const manifest = await readProjectPluginManifest(kaos, manifestPath);
if (manifest === undefined || manifest.skills === undefined) continue;
for (const skillPath of manifest.skills) {
roots.push({
path: skillPath,
source: 'extra',
plugin: {
id: manifest.name,
instructions: manifest.skillInstructions,
},
});
}
}
} catch {
// Skip directories that can't be read
}
}

return roots;
}

async function readProjectPluginManifest(
kaos: Kaos,
manifestPath: string,
): Promise<PluginManifest | undefined> {
try {
const text = await kaos.readText(manifestPath);
const parsed = JSON.parse(text) as PluginManifest;
if (typeof parsed.name !== 'string') return undefined;
return parsed;
} catch {
return undefined;
}
}
29 changes: 24 additions & 5 deletions packages/agent-core/src/rpc/core-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import { homedir } from 'node:os';

import { ErrorCodes, KimiError } from '#/errors';
import { getRootLogger, log } from '#/logging/logger';
import { PluginManager } from '#/plugin';
import { PluginManager, scanProjectPluginDirs } from '#/plugin';
import type { SkillRoot } from '../skill/types';
import { LocalFetchURLProvider } from '#/tools/providers/local-fetch-url';
import { MoonshotFetchURLProvider } from '#/tools/providers/moonshot-fetch-url';
import { MoonshotWebSearchProvider } from '#/tools/providers/moonshot-web-search';
Expand Down Expand Up @@ -302,6 +303,11 @@ export class KimiCore implements PromisableMethods<CoreAPI> {
const pluginCommands = await this.plugins.enabledCommands();
const mcpConfig = this.mergePluginMcpConfig(withCallerMcp);

// Scan project-level plugin directories from workspace local config
const projectPluginSkillRoots = localWorkspaceDirs.pluginDirs.length > 0
? await scanProjectPluginDirs(persistenceKaos, localWorkspaceDirs.pluginDirs)
: [];

// Session ctor attaches its own log sink. If anything in the setup-after-
// ctor block throws, `session.close()` releases the sink (and mcp).
const runtime = await this.resolveRuntime(config);
Expand All @@ -318,7 +324,7 @@ export class KimiCore implements PromisableMethods<CoreAPI> {
background: sessionConfig.background,
hooks: [...(config.hooks ?? []), ...this.plugins.enabledHooks()],
permissionRules: config.permission?.rules,
skills: this.resolveSessionSkillConfig(config),
skills: this.resolveSessionSkillConfig(config, projectPluginSkillRoots),
mcpConfig,
experimentalFlags: this.experimentalFlags,
imageLimits: this.imageLimits,
Expand Down Expand Up @@ -439,9 +445,16 @@ export class KimiCore implements PromisableMethods<CoreAPI> {
const pluginSessionStarts = this.plugins.enabledSessionStarts();
const pluginCommands = await this.plugins.enabledCommands();
const mcpConfig = this.mergePluginMcpConfig(withCallerMcp);

const runtime = await this.resolveRuntime(config);
const parentKaos = parentKaosForRead;
const persistenceKaos = overrides.persistenceKaos ?? parentKaos;

// Scan project-level plugin directories from workspace local config
const projectPluginSkillRoots = localWorkspaceDirs.pluginDirs.length > 0
? await scanProjectPluginDirs(persistenceKaos, localWorkspaceDirs.pluginDirs)
: [];

const session = new Session({
kaos: parentKaos.withCwd(summary.workDir),
persistenceKaos,
Expand All @@ -455,7 +468,7 @@ export class KimiCore implements PromisableMethods<CoreAPI> {
background: sessionConfig.background,
hooks: [...(config.hooks ?? []), ...this.plugins.enabledHooks()],
permissionRules: config.permission?.rules,
skills: this.resolveSessionSkillConfig(config),
skills: this.resolveSessionSkillConfig(config, projectPluginSkillRoots),
mcpConfig,
experimentalFlags: this.experimentalFlags,
imageLimits: this.imageLimits,
Expand Down Expand Up @@ -1010,14 +1023,20 @@ export class KimiCore implements PromisableMethods<CoreAPI> {
return this.kaos;
}

private resolveSessionSkillConfig(config: KimiConfig): SessionSkillConfig {
private resolveSessionSkillConfig(
config: KimiConfig,
extraProjectSkillRoots?: readonly SkillRoot[],
): SessionSkillConfig {
const explicitDirs = this.skillDirs.length > 0 ? this.skillDirs : undefined;
const pluginSkillRoots = this.plugins.pluginSkillRoots();
return {
userHomeDir: this.userHomeDir,
brandHomeDir: this.homeDir,
explicitDirs,
extraDirs: config.extraSkillDirs,
pluginSkillRoots: this.plugins.pluginSkillRoots(),
pluginSkillRoots: extraProjectSkillRoots !== undefined
? [...pluginSkillRoots, ...extraProjectSkillRoots]
: pluginSkillRoots,
mergeAllAvailableSkills: config.mergeAllAvailableSkills,
};
}
Expand Down
1 change: 1 addition & 0 deletions packages/agent-core/src/session/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,7 @@ export class Session {
projectRoot: workspace.projectRoot,
configPath: workspace.configPath,
additionalDirs: nextAdditionalDirs,
pluginDirs: [],
persisted: false,
};
}
Expand Down
2 changes: 2 additions & 0 deletions packages/agent-core/test/config/workspace-local.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ describe('workspace local config', () => {
projectRoot: root,
configPath: join(root, '.kimi-code', 'local.toml'),
additionalDirs: [],
pluginDirs: [],
});
});

Expand All @@ -68,6 +69,7 @@ describe('workspace local config', () => {
projectRoot: root,
configPath: join(root, '.kimi-code', 'local.toml'),
additionalDirs: [sharedDir, otherDir],
pluginDirs: [],
});
});

Expand Down
Loading