feat: plugin system — real manifest discovery and SDK integration

- New plugin-discovery.ts: scans ~/.claude/plugins/marketplaces/ and
  external_plugins/, reads .claude-plugin/plugin.json manifests, merges
  enabledPlugins across user/project/local settings layers with write
  escalation to local when higher layers override
- Rewrite /api/plugins and /api/plugins/[id] to use real plugin manifests
  with name@marketplace composite keys, blocklist, and multi-layer
  enable/disable with CAS-style write escalation
- SDK plugin loading delegated to SDK itself (no explicit queryOptions.plugins)
  — ensures parity with Claude CLI's settingSources resolution
- Extended init meta forwarding (plugins, mcp_servers, output_style) in
  claude-client.ts and useSSEStream.ts
- Per-provider plugin cache in agent-sdk-capabilities.ts with
  getCachedPlugins/setCachedPlugins
- Skills route: provider-scoped plugin cache lookup via sessionId param,
  external_plugins commands scan, loaded status annotation
- Updated PluginInfo type with marketplace, location, author, capability flags

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
7418andClaude Opus 4.6 committed 2026-03-12 21:28:25 +08:00
1 parent 857e9fc413
commit 7b48ee1d2a
9 files changed
+679 -187

No files matched your search

+11 -1
View File
@@ -27,7 +27,14 @@ export interface SSECallbacks {
onRewindPoint: (sdkUserMessageId: string) => void;
onKeepAlive: () => void;
onError: (accumulated: string) => void;
onInitMeta?: (meta: { tools?: unknown; slash_commands?: unknown; skills?: unknown }) => void;
onInitMeta?: (meta: {
tools?: unknown;
slash_commands?: unknown;
skills?: unknown;
plugins?: Array<{ name: string; path: string }>;
mcp_servers?: unknown;
output_style?: string;
}) => void;
}
/**
@@ -96,6 +103,9 @@ function handleSSEEvent(
tools: statusData.tools,
slash_commands: statusData.slash_commands,
skills: statusData.skills,
plugins: statusData.plugins,
mcp_servers: statusData.mcp_servers,
output_style: statusData.output_style,
});
} else if (statusData.notification) {
callbacks.onStatus(statusData.message || statusData.title || undefined);
+11 -3
View File
@@ -88,12 +88,20 @@ export function useSlashCommands(opts: {
const fetchSkills = useCallback(async () => {
let apiSkills: PopoverItem[] = [];
try {
const cwdParam = workingDirectory ? `?cwd=${encodeURIComponent(workingDirectory)}` : '';
const res = await fetch(`/api/skills${cwdParam}`);
const params = new URLSearchParams();
if (workingDirectory) params.set('cwd', workingDirectory);
if (sessionId) params.set('sessionId', sessionId);
const qs = params.toString();
const res = await fetch(`/api/skills${qs ? `?${qs}` : ''}`);
if (res.ok) {
const data = await res.json();
const skills = data.skills || [];
apiSkills = skills
.filter((s: { source?: string; loaded?: boolean }) => {
// Exclude plugin-source skills that are not loaded in the current session
if (s.source === 'plugin' && s.loaded === false) return false;
return true;
})
.map((s: { name: string; description: string; source?: "global" | "project" | "plugin" | "installed" | "sdk"; kind?: SkillKind; installedSource?: "agents" | "claude" }) => ({
label: s.name,
value: `/${s.name}`,
@@ -162,7 +170,7 @@ export function useSlashCommands(opts: {
const uniqueSkills = apiSkills.filter(s => !builtInNames.has(s.label));
return [...enrichedBuiltIns, ...uniqueSkills];
}, [workingDirectory, sdkInitMeta, enrichedBuiltIns]);
}, [sessionId, workingDirectory, sdkInitMeta, enrichedBuiltIns]);
// Insert selected item
const insertItem = useCallback((item: PopoverItem) => {