UNPKG

aiwg

Version:

Deployment tool and support utility for AI context. Copies agents, skills, commands, rules, and behaviors into the paths each AI platform reads (Claude Code, Codex, Copilot, Cursor, Warp, OpenClaw, and 6 more) so one source of truth works across 10 platfo

252 lines (214 loc) 6.12 kB
/** * Model Catalog Loader * * Loads and merges model catalogs from multiple sources: * - Builtin models (shipped with AIWG) * - Discovered models (from auto-refresh) * - Custom models (user-defined) * * Merge priority: custom > discovered > builtin * * @implements @.aiwg/architecture/enhanced-model-selection-design.md#9-model-catalog-system * @module src/catalog/loader */ import { readFile } from 'fs/promises'; import { existsSync } from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); /** * Get catalog directory in user's data directory */ function getCatalogDir() { const homeDir = process.env.HOME || process.env.USERPROFILE || ''; return path.join(homeDir, '.local/share/aiwg/catalog'); } /** * Load a catalog file, handling missing files gracefully */ async function loadCatalogFile(filePath) { if (!existsSync(filePath)) { return null; } try { const content = await readFile(filePath, 'utf-8'); return JSON.parse(content); } catch (error) { console.warn(`Failed to load catalog from ${filePath}:`, error); return null; } } /** * Merge two model catalogs * Later catalog takes precedence for conflicts */ function mergeCatalogs(base, overlay) { if (!overlay) { return base; } return { models: { ...(base.models || {}), ...(overlay.models || {}), }, providers: { ...(base.providers || {}), ...(overlay.providers || {}), }, metadata: overlay.metadata || base.metadata, }; } /** * Load the builtin models catalog */ async function loadBuiltinCatalog() { const builtinPath = path.join(__dirname, 'builtin-models.json'); const catalog = await loadCatalogFile(builtinPath); if (!catalog) { throw new Error('Failed to load builtin models catalog'); } return catalog; } /** * Load the discovered models catalog */ async function loadDiscoveredCatalog() { const catalogDir = getCatalogDir(); const discoveredPath = path.join(catalogDir, 'discovered-models.json'); return loadCatalogFile(discoveredPath); } /** * Load the custom models catalog */ async function loadCustomCatalog() { const catalogDir = getCatalogDir(); const customPath = path.join(catalogDir, 'custom-models.json'); return loadCatalogFile(customPath); } /** * Load and merge all catalog sources * * Merge order: builtin -> discovered -> custom * Later sources override earlier ones */ export async function loadCatalog() { const builtin = await loadBuiltinCatalog(); const discovered = await loadDiscoveredCatalog(); const custom = await loadCustomCatalog(); let merged = mergeCatalogs(builtin, discovered); merged = mergeCatalogs(merged, custom); // Ensure required fields exist const catalog = { models: merged.models || {}, providers: merged.providers || {}, metadata: merged.metadata || { version: '1.0.0', lastRefresh: new Date().toISOString(), modelCount: 0, providerCount: 0, }, }; // Update counts catalog.metadata.modelCount = Object.keys(catalog.models).length; catalog.metadata.providerCount = Object.keys(catalog.providers).length; return catalog; } /** * Get a single model by ID or alias */ export async function getModel(modelIdOrAlias) { const catalog = await loadCatalog(); // Try direct ID lookup if (catalog.models[modelIdOrAlias]) { return { ...catalog.models[modelIdOrAlias], id: modelIdOrAlias }; } // Try alias lookup for (const [id, model] of Object.entries(catalog.models)) { if (model.aliases.includes(modelIdOrAlias)) { return { ...model, id }; } } return null; } /** * List all models, optionally filtered */ export async function listModels(filter) { const catalog = await loadCatalog(); let models = Object.entries(catalog.models).map(([id, model]) => ({ ...model, id })); if (!filter) { return models; } // Apply filters if (filter.provider) { models = models.filter((m) => m.provider === filter.provider); } if (filter.status) { models = models.filter((m) => m.status === filter.status); } if (filter.tag) { models = models.filter((m) => m.tags.includes(filter.tag)); } if (filter.minContext) { models = models.filter((m) => m.capabilities.contextWindow >= filter.minContext); } if (filter.capability) { const cap = filter.capability; models = models.filter((m) => { const value = m.capabilities[cap]; // For boolean capabilities, must be true if (typeof value === 'boolean') { return value === true; } // For quality ratings, exclude 'poor' and 'unknown' if (typeof value === 'string') { return value !== 'poor' && value !== 'unknown'; } // For numbers, must be > 0 return value > 0; }); } return models; } /** * Get provider information */ export async function getProvider(providerId) { const catalog = await loadCatalog(); return catalog.providers[providerId] || null; } /** * List all providers */ export async function listProviders() { const catalog = await loadCatalog(); return Object.entries(catalog.providers).map(([id, info]) => ({ id, info })); } /** * Search models by query string * Searches in: id, displayName, aliases, tags */ export async function searchModels(query) { const catalog = await loadCatalog(); const lowerQuery = query.toLowerCase(); return Object.entries(catalog.models).map(([id, model]) => ({ ...model, id })).filter((model) => { // Search in ID if (model.id.toLowerCase().includes(lowerQuery)) { return true; } // Search in display name if (model.displayName.toLowerCase().includes(lowerQuery)) { return true; } // Search in aliases if (model.aliases.some((alias) => alias.toLowerCase().includes(lowerQuery))) { return true; } // Search in tags if (model.tags.some((tag) => tag.toLowerCase().includes(lowerQuery))) { return true; } return false; }); }