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
217 lines • 6.48 kB
JavaScript
/**
* 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];
}
// Try alias lookup
for (const model of Object.values(catalog.models)) {
if (model.aliases.includes(modelIdOrAlias)) {
return model;
}
}
return null;
}
/**
* List all models, optionally filtered
*/
export async function listModels(filter) {
const catalog = await loadCatalog();
let models = Object.values(catalog.models);
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 !== undefined) {
const minCtx = filter.minContext;
models = models.filter((m) => m.capabilities.contextWindow >= minCtx);
}
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.values(catalog.models).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;
});
}
//# sourceMappingURL=loader.js.map