feat(skills-catalog): implement caching, curated sources, git operations, and skill installation

This commit is contained in:
Bohdan Triapitsyn
2025-12-30 21:03:25 +02:00
parent 92794d1810
commit a236dc81c9
28 changed files with 4790 additions and 7 deletions
+84
View File
@@ -412,3 +412,87 @@ export interface RuntimeAPIs {
}
export type RuntimeAPISelector<TValue> = (apis: RuntimeAPIs) => TValue;
// ============== Skills Catalog Types ==============
export type SkillsCatalogSourceId = string;
export interface SkillsCatalogSource {
id: SkillsCatalogSourceId;
label: string;
description?: string;
source: string;
defaultSubpath?: string;
}
export interface SkillsCatalogItemInstalledBadge {
isInstalled: boolean;
scope?: 'user' | 'project';
}
export interface SkillsCatalogItem {
sourceId: SkillsCatalogSourceId;
repoSource: string;
repoSubpath?: string;
gitIdentityId?: string;
skillDir: string;
skillName: string;
frontmatterName?: string;
description?: string;
installable: boolean;
warnings?: string[];
installed?: SkillsCatalogItemInstalledBadge;
}
export interface SkillsCatalogResponse {
ok: boolean;
sources?: SkillsCatalogSource[];
itemsBySource?: Record<SkillsCatalogSourceId, SkillsCatalogItem[]>;
error?: { kind: string; message: string };
}
export interface SkillsRepoScanRequest {
source: string;
subpath?: string;
gitIdentityId?: string;
}
export type SkillsRepoScanError =
| { kind: 'authRequired'; message: string; sshOnly: true; identities?: Array<{ id: string; name: string }> }
| { kind: 'invalidSource'; message: string }
| { kind: 'gitUnavailable'; message: string }
| { kind: 'networkError'; message: string }
| { kind: 'unknown'; message: string };
export interface SkillsRepoScanResponse {
ok: boolean;
items?: SkillsCatalogItem[];
error?: SkillsRepoScanError;
}
export interface SkillsInstallSelection {
skillDir: string;
}
export interface SkillsInstallRequest {
source: string;
subpath?: string;
gitIdentityId?: string;
scope: 'user' | 'project';
selections: SkillsInstallSelection[];
conflictPolicy?: 'prompt' | 'skipAll' | 'overwriteAll';
conflictDecisions?: Record<string, 'skip' | 'overwrite'>;
}
export type SkillsInstallError = SkillsRepoScanError | {
kind: 'conflicts';
message: string;
conflicts: Array<{ skillName: string; scope: 'user' | 'project' }>;
};
export interface SkillsInstallResponse {
ok: boolean;
installed?: Array<{ skillName: string; scope: 'user' | 'project' }>;
skipped?: Array<{ skillName: string; reason: string }>;
error?: SkillsInstallError;
}
+11
View File
@@ -27,6 +27,14 @@ export type DesktopServerInfo = {
cliAvailable: boolean;
};
export type SkillCatalogConfig = {
id: string;
label: string;
source: string;
subpath?: string;
gitIdentityId?: string;
};
export type DesktopSettings = {
themeId?: string;
useSystemTheme?: boolean;
@@ -44,6 +52,9 @@ export type DesktopSettings = {
defaultModel?: string; // format: "provider/model"
defaultAgent?: string;
queueModeEnabled?: boolean;
// User-added skills catalogs (persisted to ~/.config/openchamber/settings.json)
skillCatalogs?: SkillCatalogConfig[];
};
export type DesktopSettingsApi = {
+39
View File
@@ -44,6 +44,40 @@ type PersistApi = {
onFinishHydration?: (callback: () => void) => (() => void) | void;
};
const sanitizeSkillCatalogs = (value: unknown): DesktopSettings['skillCatalogs'] | undefined => {
if (!Array.isArray(value)) {
return undefined;
}
const result: NonNullable<DesktopSettings['skillCatalogs']> = [];
const seen = new Set<string>();
for (const entry of value) {
if (!entry || typeof entry !== 'object') continue;
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() : '';
const gitIdentityId = typeof candidate.gitIdentityId === 'string' ? candidate.gitIdentityId.trim() : '';
if (!id || !label || !source) continue;
if (seen.has(id)) continue;
seen.add(id);
result.push({
id,
label,
source,
...(subpath ? { subpath } : {}),
...(gitIdentityId ? { gitIdentityId } : {}),
});
}
return result;
};
const getPersistApi = (): PersistApi | undefined => {
const candidate = (useUIStore as unknown as { persist?: PersistApi }).persist;
if (candidate && typeof candidate === 'object') {
@@ -140,6 +174,11 @@ const sanitizeWebSettings = (payload: unknown): DesktopSettings | null => {
result.queueModeEnabled = candidate.queueModeEnabled;
}
const skillCatalogs = sanitizeSkillCatalogs(candidate.skillCatalogs);
if (skillCatalogs) {
result.skillCatalogs = skillCatalogs;
}
return result;
};