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:
1 parent
857e9fc413
commit
7b48ee1d2a
9 files changed
+679
-187
No files matched your search
@@ -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);
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
Reference in new issue
Block a user