Files
openchamber/packages/vscode/src/bridge-config-runtime.ts
T
Bohdan Triapitsyn 2031e3b4a8 Decouple bundled UI from runtime API and add remote instance tooling (#1228)
Add a packaged-client runtime boundary so the shared UI can talk to local,
desktop, remote, and VS Code runtimes through the right transport instead of
assuming one same-origin web server.

Centralize OpenChamber-owned API access behind RuntimeAPIs, runtimeFetch, and
runtime URL helpers, while keeping official OpenCode traffic on the SDK path.
Support runtime switching, remote host selection, desktop client credentials,
and headless connection links for pairing packaged clients with remote
OpenChamber servers.

Harden the new auth model by moving long-lived client tokens out of browser
URLs, introducing short-lived scoped URL tokens for browser-owned transports,
restricting URL-token access to explicit readable/realtime routes, and making
client-token management session-scoped or self-scoped as appropriate.

Update browser-owned assets and preview proxy flows to work with the split
runtime model, including authenticated project icons, preview token propagation,
CSP-safe preview bridge injection, and preview proxy auth that survives
short-lived URL-token expiry.

Tighten Electron security boundaries for packaged clients by gating privileged
preload state to trusted origins and requiring explicit confirmation before
connect deep-links import or switch remote runtimes.

Also refresh agent guidance and project skills so future runtime/API, auth,
preview, UI, CLI, settings, locale, and drag-to-reorder work follows the new
architecture.
2026-06-02 00:43:05 +03:00

852 lines
31 KiB
TypeScript

import * as vscode from 'vscode';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import {
createAgent,
createCommand,
createSnippet,
deleteAgent,
deleteCommand,
deleteSnippet,
getAgentSources,
getCommandSources,
getSnippet,
updateAgent,
updateCommand,
updateSnippet,
type AgentScope,
type CommandScope,
AGENT_SCOPE,
COMMAND_SCOPE,
discoverSkills,
mergeDiscoveredSkills,
getSkillSources,
createSkill,
updateSkill,
deleteSkill,
readSkillSupportingFile,
writeSkillSupportingFile,
deleteSkillSupportingFile,
type SkillScope,
type DiscoveredSkill,
SKILL_SCOPE,
listMcpConfigs,
listPluginDirFiles,
listPluginEntries,
getPluginEntry,
createPluginEntry,
updatePluginEntry,
deletePluginEntry,
readPluginDirFile,
writePluginDirFile,
deletePluginDirFile,
queryPluginRegistry,
listSnippets,
getMcpConfig,
createMcpConfig,
updateMcpConfig,
deleteMcpConfig,
expandSnippets,
type SnippetScope,
} from './opencodeConfig';
import {
getSkillsCatalog,
scanSkillsRepository as scanSkillsRepositoryFromGit,
installSkillsFromRepository as installSkillsFromGit,
type SkillsCatalogSourceConfig,
} from './skillsCatalog';
import type { BridgeContext, BridgeResponse } from './bridge';
type BridgeMessageInput = {
id: string;
type: string;
payload?: unknown;
};
type ConfigRuntimeDeps = {
readSettings: (ctx?: BridgeContext) => Record<string, unknown>;
persistSettings: (changes: Record<string, unknown>, ctx?: BridgeContext) => Promise<Record<string, unknown>>;
readMagicPromptOverrides: () => { version: number; overrides: Record<string, string> };
saveMagicPromptOverride: (id: string, text: string) => Promise<{ version: number; overrides: Record<string, string> }>;
resetMagicPromptOverride: (id: string) => Promise<{ version: number; overrides: Record<string, string> }>;
resetAllMagicPromptOverrides: () => Promise<{ version: number; overrides: Record<string, string> }>;
fetchOpenCodeSkillsFromApi: (ctx: BridgeContext | undefined, workingDirectory?: string) => Promise<DiscoveredSkill[] | null>;
clientReloadDelayMs: number;
};
const AGENTS_MD_PATH = path.join(os.homedir(), '.config', 'opencode', 'AGENTS.md');
const MAX_BEHAVIOR_PROMPT_SIZE = 1024 * 1024;
const resolveWorkingDirectory = (ctx: BridgeContext | undefined, directory?: string): string | undefined => (
(typeof directory === 'string' && directory.trim())
? directory.trim()
: (ctx?.manager?.getWorkingDirectory() || vscode.workspace.workspaceFolders?.[0]?.uri.fsPath)
);
const pluginMutationPayload = async (
ctx: BridgeContext | undefined,
deps: ConfigRuntimeDeps,
label: string,
) => {
try {
await ctx?.manager?.restart();
return {
success: true,
requiresReload: true,
message: `${label}. Reloading interface…`,
reloadDelayMs: deps.clientReloadDelayMs,
reloadFailed: false,
};
} catch (error) {
return {
success: true,
requiresReload: false,
message: `${label}, but OpenCode reload failed.`,
reloadDelayMs: deps.clientReloadDelayMs,
reloadFailed: true,
warning: error instanceof Error ? error.message : String(error),
};
}
};
const parseSkillsCatalogSources = (settings: Record<string, unknown>): SkillsCatalogSourceConfig[] => {
const rawCatalogs = (settings as { skillCatalogs?: unknown }).skillCatalogs;
if (!Array.isArray(rawCatalogs)) {
return [];
}
return rawCatalogs
.map((entry) => {
if (!entry || typeof entry !== 'object') return null;
const candidate = entry as Record<string, unknown>;
const id = typeof candidate.id === 'string' ? candidate.id.trim() : '';
const label = typeof candidate.label === 'string' ? candidate.label.trim() : '';
const source = typeof candidate.source === 'string' ? candidate.source.trim() : '';
const subpath = typeof candidate.subpath === 'string' ? candidate.subpath.trim() : '';
if (!id || !label || !source) return null;
const normalized: SkillsCatalogSourceConfig = {
id,
label,
description: source,
source,
...(subpath ? { defaultSubpath: subpath } : {}),
};
return normalized;
})
.filter((value): value is SkillsCatalogSourceConfig => value !== null);
};
const resolveDiscoveredSkills = async (
deps: ConfigRuntimeDeps,
ctx: BridgeContext | undefined,
workingDirectory?: string,
): Promise<DiscoveredSkill[]> => mergeDiscoveredSkills(
(await deps.fetchOpenCodeSkillsFromApi(ctx, workingDirectory)) || [],
discoverSkills(workingDirectory),
);
export async function handleConfigBridgeMessage(
message: BridgeMessageInput,
ctx: BridgeContext | undefined,
deps: ConfigRuntimeDeps,
): Promise<BridgeResponse | null> {
const { id, type, payload } = message;
switch (type) {
case 'api:config/opencode-resolution:get': {
const debugInfo = ctx?.manager?.getDebugInfo();
const configuredFromWorkspace = vscode.workspace.getConfiguration('openchamber').get<string>('opencodeBinary');
const configured = typeof configuredFromWorkspace === 'string' && configuredFromWorkspace.trim().length > 0
? configuredFromWorkspace.trim()
: null;
const resolved = debugInfo?.cliPath ?? null;
const source = (() => {
if (!resolved) return null;
if (configured && configured === resolved) return 'settings';
const envBinary = typeof process.env.OPENCODE_BINARY === 'string' ? process.env.OPENCODE_BINARY.trim() : '';
if (envBinary && envBinary === resolved) return 'env';
return 'path';
})();
return {
id,
type,
success: true,
data: {
configured,
resolved,
resolvedDir: resolved ? path.dirname(resolved) : null,
source,
detectedNow: resolved,
detectedSourceNow: source,
shim: null,
viaWsl: false,
wslBinary: null,
wslPath: null,
wslDistro: null,
node: process.execPath || null,
bun: null,
},
};
}
case 'api:config/settings:get': {
const settings = deps.readSettings(ctx);
return { id, type, success: true, data: settings };
}
case 'api:config/settings:save': {
const changes = (payload as Record<string, unknown>) || {};
const updated = await deps.persistSettings(changes, ctx);
return { id, type, success: true, data: updated };
}
case 'api:behavior/agents-md:get': {
try {
const content = await fs.promises.readFile(AGENTS_MD_PATH, 'utf8');
return { id, type, success: true, data: { content, exists: true } };
} catch (error) {
if ((error as NodeJS.ErrnoException)?.code === 'ENOENT') {
return { id, type, success: true, data: { content: '', exists: false } };
}
throw error;
}
}
case 'api:behavior/agents-md:save': {
const request = (payload || {}) as { content?: unknown };
const content = typeof request.content === 'string' ? request.content : '';
if (content.length > MAX_BEHAVIOR_PROMPT_SIZE) {
return { id, type, success: false, error: `Content exceeds maximum size of ${MAX_BEHAVIOR_PROMPT_SIZE} bytes` };
}
await fs.promises.mkdir(path.dirname(AGENTS_MD_PATH), { recursive: true });
await fs.promises.writeFile(AGENTS_MD_PATH, content, 'utf8');
await ctx?.manager?.restart();
return { id, type, success: true, data: { success: true } };
}
case 'api:magic-prompts:get': {
return { id, type, success: true, data: deps.readMagicPromptOverrides() };
}
case 'api:magic-prompts:save': {
const request = (payload || {}) as { id?: string; text?: string };
const promptId = typeof request.id === 'string' ? request.id : '';
if (!promptId) {
return { id, type, success: false, error: 'Prompt id is required' };
}
if (typeof request.text !== 'string') {
return { id, type, success: false, error: 'Prompt text is required' };
}
const data = await deps.saveMagicPromptOverride(promptId, request.text);
return { id, type, success: true, data };
}
case 'api:magic-prompts:reset': {
const request = (payload || {}) as { id?: string };
const promptId = typeof request.id === 'string' ? request.id : '';
if (!promptId) {
return { id, type, success: false, error: 'Prompt id is required' };
}
const data = await deps.resetMagicPromptOverride(promptId);
return { id, type, success: true, data };
}
case 'api:magic-prompts:reset-all': {
const data = await deps.resetAllMagicPromptOverrides();
return { id, type, success: true, data };
}
case 'api:config/reload': {
await ctx?.manager?.restart();
return { id, type, success: true, data: { restarted: true } };
}
case 'api:config/agents': {
const { method, name, body, directory } = (payload || {}) as {
method?: string;
name?: string;
body?: Record<string, unknown>;
directory?: string;
};
const agentName = typeof name === 'string' ? name.trim() : '';
if (!agentName) {
return { id, type, success: false, error: 'Agent name is required' };
}
const workingDirectory = resolveWorkingDirectory(ctx, directory);
const normalizedMethod = typeof method === 'string' && method.trim() ? method.trim().toUpperCase() : 'GET';
if (normalizedMethod === 'GET') {
const sources = getAgentSources(agentName, workingDirectory);
const scope = sources.md.exists
? sources.md.scope
: (sources.json.exists ? sources.json.scope : null);
return {
id,
type,
success: true,
data: { name: agentName, sources, scope, isBuiltIn: !sources.md.exists && !sources.json.exists },
};
}
if (normalizedMethod === 'POST') {
const scopeValue = body?.scope as string | undefined;
const scope: AgentScope | undefined = scopeValue === 'project' ? AGENT_SCOPE.PROJECT : scopeValue === 'user' ? AGENT_SCOPE.USER : undefined;
createAgent(agentName, (body || {}) as Record<string, unknown>, workingDirectory, scope);
await ctx?.manager?.restart();
return {
id,
type,
success: true,
data: {
success: true,
requiresReload: true,
message: `Agent ${agentName} created successfully. Reloading interface…`,
reloadDelayMs: deps.clientReloadDelayMs,
},
};
}
if (normalizedMethod === 'PATCH') {
updateAgent(agentName, (body || {}) as Record<string, unknown>, workingDirectory);
await ctx?.manager?.restart();
return {
id,
type,
success: true,
data: {
success: true,
requiresReload: true,
message: `Agent ${agentName} updated successfully. Reloading interface…`,
reloadDelayMs: deps.clientReloadDelayMs,
},
};
}
if (normalizedMethod === 'DELETE') {
deleteAgent(agentName, workingDirectory);
await ctx?.manager?.restart();
return {
id,
type,
success: true,
data: {
success: true,
requiresReload: true,
message: `Agent ${agentName} deleted successfully. Reloading interface…`,
reloadDelayMs: deps.clientReloadDelayMs,
},
};
}
return { id, type, success: false, error: `Unsupported method: ${normalizedMethod}` };
}
case 'api:config/commands': {
const { method, name, body, directory } = (payload || {}) as {
method?: string;
name?: string;
body?: Record<string, unknown>;
directory?: string;
};
const commandName = typeof name === 'string' ? name.trim() : '';
if (!commandName) {
return { id, type, success: false, error: 'Command name is required' };
}
const workingDirectory = resolveWorkingDirectory(ctx, directory);
const normalizedMethod = typeof method === 'string' && method.trim() ? method.trim().toUpperCase() : 'GET';
if (normalizedMethod === 'GET') {
const sources = getCommandSources(commandName, workingDirectory);
const scope = sources.md.exists
? sources.md.scope
: (sources.json.exists ? sources.json.scope : null);
return {
id,
type,
success: true,
data: { name: commandName, sources, scope, isBuiltIn: !sources.md.exists && !sources.json.exists },
};
}
if (normalizedMethod === 'POST') {
const scopeValue = body?.scope as string | undefined;
const scope: CommandScope | undefined = scopeValue === 'project' ? COMMAND_SCOPE.PROJECT : scopeValue === 'user' ? COMMAND_SCOPE.USER : undefined;
createCommand(commandName, (body || {}) as Record<string, unknown>, workingDirectory, scope);
await ctx?.manager?.restart();
return {
id,
type,
success: true,
data: {
success: true,
requiresReload: true,
message: `Command ${commandName} created successfully. Reloading interface…`,
reloadDelayMs: deps.clientReloadDelayMs,
},
};
}
if (normalizedMethod === 'PATCH') {
updateCommand(commandName, (body || {}) as Record<string, unknown>, workingDirectory);
await ctx?.manager?.restart();
return {
id,
type,
success: true,
data: {
success: true,
requiresReload: true,
message: `Command ${commandName} updated successfully. Reloading interface…`,
reloadDelayMs: deps.clientReloadDelayMs,
},
};
}
if (normalizedMethod === 'DELETE') {
deleteCommand(commandName, workingDirectory);
await ctx?.manager?.restart();
return {
id,
type,
success: true,
data: {
success: true,
requiresReload: true,
message: `Command ${commandName} deleted successfully. Reloading interface…`,
reloadDelayMs: deps.clientReloadDelayMs,
},
};
}
return { id, type, success: false, error: `Unsupported method: ${normalizedMethod}` };
}
case 'api:config/mcp': {
const { method, name, body, directory } = (payload || {}) as {
method?: string;
name?: string;
body?: Record<string, unknown>;
directory?: string;
};
const normalizedMethod = typeof method === 'string' && method.trim() ? method.trim().toUpperCase() : 'GET';
const mcpName = typeof name === 'string' ? name.trim() : '';
const workingDirectory = resolveWorkingDirectory(ctx, directory);
if (normalizedMethod === 'GET' && !mcpName) {
const configs = listMcpConfigs(workingDirectory);
return { id, type, success: true, data: configs };
}
if (!mcpName) {
return { id, type, success: false, error: 'MCP server name is required' };
}
if (normalizedMethod === 'GET') {
const config = getMcpConfig(mcpName, workingDirectory);
if (!config) {
return { id, type, success: false, error: `MCP server "${mcpName}" not found` };
}
return { id, type, success: true, data: config };
}
if (normalizedMethod === 'POST') {
const scope = body?.scope as 'user' | 'project' | undefined;
createMcpConfig(mcpName, (body || {}) as Record<string, unknown>, workingDirectory, scope);
await ctx?.manager?.restart();
return {
id,
type,
success: true,
data: {
success: true,
requiresReload: true,
message: `MCP server "${mcpName}" created. Reloading interface…`,
reloadDelayMs: deps.clientReloadDelayMs,
},
};
}
if (normalizedMethod === 'PATCH') {
updateMcpConfig(mcpName, (body || {}) as Record<string, unknown>, workingDirectory);
await ctx?.manager?.restart();
return {
id,
type,
success: true,
data: {
success: true,
requiresReload: true,
message: `MCP server "${mcpName}" updated. Reloading interface…`,
reloadDelayMs: deps.clientReloadDelayMs,
},
};
}
if (normalizedMethod === 'DELETE') {
deleteMcpConfig(mcpName, workingDirectory);
await ctx?.manager?.restart();
return {
id,
type,
success: true,
data: {
success: true,
requiresReload: true,
message: `MCP server "${mcpName}" deleted. Reloading interface…`,
reloadDelayMs: deps.clientReloadDelayMs,
},
};
}
return { id, type, success: false, error: `Unsupported method: ${normalizedMethod}` };
}
case 'api:config/plugins': {
const { method, target, pluginId, body, directory, specs, refresh } = (payload || {}) as {
method?: string;
target?: 'list' | 'registry' | 'entry' | 'file';
pluginId?: string;
body?: Record<string, unknown>;
directory?: string;
specs?: string[];
refresh?: boolean;
};
const normalizedMethod = typeof method === 'string' && method.trim() ? method.trim().toUpperCase() : 'GET';
const workingDirectory = resolveWorkingDirectory(ctx, directory);
if ((target === 'list' || !target) && normalizedMethod === 'GET') {
return {
id,
type,
success: true,
data: {
entries: listPluginEntries(workingDirectory),
files: listPluginDirFiles(workingDirectory),
},
};
}
if (target === 'registry' && normalizedMethod === 'GET') {
const data = await queryPluginRegistry(Array.isArray(specs) ? specs : [], {
refresh: refresh === true,
workingDirectory,
});
return { id, type, success: true, data };
}
if (target === 'entry') {
if (normalizedMethod === 'GET') {
if (!pluginId) return { id, type, success: false, error: 'Plugin entry id is required' };
const entry = getPluginEntry(pluginId, workingDirectory);
if (!entry) return { id, type, success: false, error: 'Plugin entry not found' };
return { id, type, success: true, data: entry };
}
if (normalizedMethod === 'POST') {
createPluginEntry(body || {}, workingDirectory);
} else if (normalizedMethod === 'PATCH') {
if (!pluginId) return { id, type, success: false, error: 'Plugin entry id is required' };
updatePluginEntry(pluginId, body || {}, workingDirectory);
} else if (normalizedMethod === 'DELETE') {
if (!pluginId) return { id, type, success: false, error: 'Plugin entry id is required' };
deletePluginEntry(pluginId, workingDirectory);
} else {
return { id, type, success: false, error: `Unsupported method: ${normalizedMethod}` };
}
return {
id,
type,
success: true,
data: await pluginMutationPayload(ctx, deps, 'Plugin entry changed'),
};
}
if (target === 'file') {
if (normalizedMethod === 'GET') {
if (!pluginId) return { id, type, success: false, error: 'Plugin file id is required' };
const file = readPluginDirFile(pluginId, workingDirectory);
if (!file) return { id, type, success: false, error: 'Plugin file not found' };
return { id, type, success: true, data: file };
}
if (normalizedMethod === 'POST') {
writePluginDirFile(body || {}, workingDirectory);
} else if (normalizedMethod === 'PUT') {
if (!pluginId) return { id, type, success: false, error: 'Plugin file id is required' };
const existing = readPluginDirFile(pluginId, workingDirectory);
if (!existing) return { id, type, success: false, error: 'Plugin file not found' };
writePluginDirFile({ fileName: existing.fileName, scope: existing.scope, content: body?.content }, workingDirectory, { overwrite: true });
} else if (normalizedMethod === 'DELETE') {
if (!pluginId) return { id, type, success: false, error: 'Plugin file id is required' };
deletePluginDirFile(pluginId, workingDirectory);
} else {
return { id, type, success: false, error: `Unsupported method: ${normalizedMethod}` };
}
return {
id,
type,
success: true,
data: await pluginMutationPayload(ctx, deps, 'Plugin file changed'),
};
}
return { id, type, success: false, error: 'Unsupported plugin config request' };
}
case 'api:config/snippets': {
const { method, name, body, directory } = (payload || {}) as {
method?: string;
name?: string;
body?: Record<string, unknown>;
directory?: string;
};
const normalizedMethod = typeof method === 'string' && method.trim() ? method.trim().toUpperCase() : 'GET';
const snippetName = typeof name === 'string' ? name.trim() : '';
const workingDirectory = resolveWorkingDirectory(ctx, directory);
if (normalizedMethod === 'GET' && !snippetName) {
return { id, type, success: true, data: listSnippets(workingDirectory) };
}
if (normalizedMethod === 'POST' && !snippetName) {
return { id, type, success: true, data: { text: expandSnippets(typeof body?.text === 'string' ? body.text : '', workingDirectory) } };
}
if (!snippetName) {
return { id, type, success: false, error: 'Snippet name is required' };
}
if (normalizedMethod === 'GET') {
const snippet = getSnippet(snippetName, workingDirectory);
if (!snippet) return { id, type, success: false, error: `Snippet "${snippetName}" not found` };
return { id, type, success: true, data: snippet };
}
if (normalizedMethod === 'POST') {
const scope = body?.scope === 'project' ? 'project' : 'global';
const snippet = createSnippet(snippetName, (body || {}) as Record<string, unknown>, workingDirectory, scope as SnippetScope);
return { id, type, success: true, data: { success: true, snippet } };
}
if (normalizedMethod === 'PATCH') {
const snippet = updateSnippet(snippetName, (body || {}) as Record<string, unknown>, workingDirectory);
return { id, type, success: true, data: { success: true, snippet } };
}
if (normalizedMethod === 'DELETE') {
deleteSnippet(snippetName, workingDirectory);
return { id, type, success: true, data: { success: true } };
}
return { id, type, success: false, error: `Unsupported method: ${normalizedMethod}` };
}
case 'api:config/skills': {
const { method, name, body } = (payload || {}) as { method?: string; name?: string; body?: Record<string, unknown> };
const workingDirectory = ctx?.manager?.getWorkingDirectory() || vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
const normalizedMethod = typeof method === 'string' && method.trim() ? method.trim().toUpperCase() : 'GET';
if (!name && normalizedMethod === 'GET') {
const skills = await resolveDiscoveredSkills(deps, ctx, workingDirectory);
return { id, type, success: true, data: { skills } };
}
const skillName = typeof name === 'string' ? name.trim() : '';
if (!skillName) {
return { id, type, success: false, error: 'Skill name is required' };
}
if (normalizedMethod === 'GET') {
const discoveredSkill = (await resolveDiscoveredSkills(deps, ctx, workingDirectory))
.find((skill) => skill.name === skillName);
const sources = getSkillSources(skillName, workingDirectory, discoveredSkill || null);
return {
id,
type,
success: true,
data: { name: skillName, sources, scope: sources.md.scope, source: sources.md.source },
};
}
if (normalizedMethod === 'POST') {
const scopeValue = body?.scope as string | undefined;
const sourceValue = body?.source as string | undefined;
const scope: SkillScope | undefined = scopeValue === 'project' ? SKILL_SCOPE.PROJECT : scopeValue === 'user' ? SKILL_SCOPE.USER : undefined;
const normalizedSource = sourceValue === 'agents' ? 'agents' : 'opencode';
createSkill(skillName, { ...(body || {}), source: normalizedSource } as Record<string, unknown>, workingDirectory, scope);
await ctx?.manager?.restart();
return {
id,
type,
success: true,
data: {
success: true,
requiresReload: true,
message: `Skill ${skillName} created successfully. Reloading interface…`,
reloadDelayMs: deps.clientReloadDelayMs,
},
};
}
if (normalizedMethod === 'PATCH') {
updateSkill(skillName, (body || {}) as Record<string, unknown>, workingDirectory);
await ctx?.manager?.restart();
return {
id,
type,
success: true,
data: {
success: true,
requiresReload: true,
message: `Skill ${skillName} updated successfully. Reloading interface…`,
reloadDelayMs: deps.clientReloadDelayMs,
},
};
}
if (normalizedMethod === 'DELETE') {
deleteSkill(skillName, workingDirectory);
await ctx?.manager?.restart();
return {
id,
type,
success: true,
data: {
success: true,
requiresReload: true,
message: `Skill ${skillName} deleted successfully. Reloading interface…`,
reloadDelayMs: deps.clientReloadDelayMs,
},
};
}
return { id, type, success: false, error: `Unsupported method: ${normalizedMethod}` };
}
case 'api:config/skills:catalog': {
const refresh = Boolean((payload as { refresh?: boolean } | undefined)?.refresh);
const workingDirectory = ctx?.manager?.getWorkingDirectory() || vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
const settings = deps.readSettings(ctx);
const additionalSources = parseSkillsCatalogSources(settings);
const installedSkills = await resolveDiscoveredSkills(deps, ctx, workingDirectory);
const data = await getSkillsCatalog(workingDirectory, refresh, additionalSources, installedSkills);
return { id, type, success: true, data };
}
case 'api:config/skills:scan': {
const body = (payload || {}) as { source?: string; subpath?: string; gitIdentityId?: string };
const data = await scanSkillsRepositoryFromGit({
source: String(body.source || ''),
subpath: body.subpath,
});
return { id, type, success: true, data };
}
case 'api:config/skills:install': {
const body = (payload || {}) as {
source?: string;
subpath?: string;
scope?: 'user' | 'project';
targetSource?: 'opencode' | 'agents';
selections?: Array<{ skillDir: string }>;
conflictPolicy?: 'prompt' | 'skipAll' | 'overwriteAll';
conflictDecisions?: Record<string, 'skip' | 'overwrite'>;
};
const workingDirectory = ctx?.manager?.getWorkingDirectory() || vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
const data = await installSkillsFromGit({
source: String(body.source || ''),
subpath: body.subpath,
scope: body.scope === 'project' ? 'project' : 'user',
targetSource: body.targetSource === 'agents' ? 'agents' : 'opencode',
workingDirectory: body.scope === 'project' ? workingDirectory : undefined,
selections: Array.isArray(body.selections) ? body.selections : [],
conflictPolicy: body.conflictPolicy,
conflictDecisions: body.conflictDecisions,
});
if (data.ok) {
const installed = data.installed || [];
const skipped = data.skipped || [];
const requiresReload = installed.length > 0;
if (requiresReload) {
await ctx?.manager?.restart();
}
return {
id,
type,
success: true,
data: {
ok: true,
installed,
skipped,
requiresReload,
message: requiresReload ? 'Skills installed successfully. Reloading interface…' : 'No skills were installed',
reloadDelayMs: requiresReload ? deps.clientReloadDelayMs : undefined,
},
};
}
return { id, type, success: true, data };
}
case 'api:config/skills/files': {
const { method, name, filePath, content } = (payload || {}) as {
method?: string;
name?: string;
filePath?: string;
content?: string;
};
const workingDirectory = ctx?.manager?.getWorkingDirectory() || vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
const skillName = typeof name === 'string' ? name.trim() : '';
if (!skillName) {
return { id, type, success: false, error: 'Skill name is required' };
}
const relativePath = typeof filePath === 'string' ? filePath.trim() : '';
if (!relativePath) {
return { id, type, success: false, error: 'File path is required' };
}
const discoveredSkill = (await resolveDiscoveredSkills(deps, ctx, workingDirectory))
.find((skill) => skill.name === skillName);
const sources = getSkillSources(skillName, workingDirectory, discoveredSkill || null);
if (!sources.md.dir) {
return { id, type, success: false, error: `Skill "${skillName}" not found` };
}
const skillDir = sources.md.dir;
const normalizedMethod = typeof method === 'string' && method.trim() ? method.trim().toUpperCase() : 'GET';
if (normalizedMethod === 'GET') {
const fileContent = readSkillSupportingFile(skillDir, relativePath);
if (fileContent === null) {
return { id, type, success: false, error: `File "${relativePath}" not found in skill "${skillName}"` };
}
return { id, type, success: true, data: { content: fileContent } };
}
if (normalizedMethod === 'PUT') {
writeSkillSupportingFile(skillDir, relativePath, content || '');
return { id, type, success: true, data: { success: true } };
}
if (normalizedMethod === 'DELETE') {
deleteSkillSupportingFile(skillDir, relativePath);
return { id, type, success: true, data: { success: true } };
}
return { id, type, success: false, error: `Unsupported method: ${normalizedMethod}` };
}
default:
return null;
}
}