feat(skills-catalog): implement caching, curated sources, git operations, and skill installation
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user