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

263 lines 9.94 kB
/** * MCP Profile Registry (TypeScript) * * Named, ordered subsets of registered MCP servers. * Stored in ~/.aiwg/mcp-profiles.json. * * This module is used for type checking and vitest. * profiles.mjs is the runtime ESM version used by cli.mjs. * * @implements #889 */ import { readFile, writeFile, mkdir } from 'fs/promises'; import { resolve } from 'path'; import { resolveConfigDir } from '../config/user-config.js'; // ───────────────────────────────────────────── // Constants // ───────────────────────────────────────────── const PROFILES_FILENAME = 'mcp-profiles.json'; const RESERVED_NAMES = new Set(['all', 'none', 'default']); const NAME_RE = /^[a-z0-9-]+$/; const DEFAULT_DATA = { apiVersion: 'aiwg.io/v1', kind: 'McpProfileRegistry', profiles: {}, }; // ───────────────────────────────────────────── // Preset profiles // ───────────────────────────────────────────── export const PRESET_PROFILES = { minimal: { description: 'Minimal toolset for smoke tests (~6K token budget)', servers: [], providerOverrides: {}, }, dev: { description: 'Code editing + git + memory (~12K token budget)', servers: ['git-gitea', 'codeindex-codehound', 'memory-fortemi'], providerOverrides: { codex: { toolDeny: ['git-gitea__delete_*', 'git-gitea__actions_config_write'] }, }, }, ops: { description: 'Infra + git + CMDB operations (~14K token budget)', servers: ['git-gitea', 'cmdb-itassets', 'memory-fortemi'], providerOverrides: {}, }, research: { description: 'Documentation + memory + calendar (~10K token budget)', servers: ['memory-fortemi', 'claude_ai_Google_Drive', 'claude_ai_Google_Calendar'], providerOverrides: {}, }, incident: { description: 'Incident response — git + CMDB + memory (~16K token budget)', servers: ['git-gitea', 'cmdb-itassets', 'memory-fortemi', 'codeindex-codehound'], providerOverrides: {}, }, full: { description: 'All registered servers — for exploration (~21K token budget)', servers: ['__all__'], providerOverrides: {}, }, }; // ───────────────────────────────────────────── // Registry class // ───────────────────────────────────────────── export class McpProfileRegistry { configDir; cache = null; constructor(configDirOverride) { this.configDir = resolveConfigDir(configDirOverride); } getPath() { return resolve(this.configDir, PROFILES_FILENAME); } async load() { if (this.cache) return this.cache; const filePath = this.getPath(); try { const content = await readFile(filePath, 'utf-8'); const parsed = JSON.parse(content); this.cache = { ...DEFAULT_DATA, ...parsed, profiles: parsed.profiles || {}, }; } catch { this.cache = { ...DEFAULT_DATA, profiles: {} }; } return this.cache; } async save() { if (!this.cache) return; await mkdir(this.configDir, { recursive: true }); await writeFile(this.getPath(), JSON.stringify(this.cache, null, 2) + '\n', 'utf-8'); } validateName(name) { if (!NAME_RE.test(name)) { throw new Error(`Invalid profile name "${name}". Names must match [a-z0-9-]+.`); } if (RESERVED_NAMES.has(name)) { throw new Error(`"${name}" is a reserved profile name. Choose a different name.`); } } async validateServers(serverNames, serverRegistry) { if (!serverRegistry) return; const missing = []; for (const name of serverNames) { if (name === '__all__') continue; const server = await serverRegistry.get(name); if (!server) missing.push(name); } if (missing.length > 0) { throw new Error(`Server(s) not found in registry: ${missing.join(', ')}.\n` + `Use "aiwg mcp list" to see registered servers.`); } } async add(profile, serverRegistry) { this.validateName(profile.name); const data = await this.load(); if (data.profiles[profile.name]) { throw new Error(`Profile "${profile.name}" already exists. Use "aiwg mcp profile edit" to modify it.`); } await this.validateServers(profile.servers ?? [], serverRegistry); data.profiles[profile.name] = { ...profile, servers: profile.servers ?? [], providerOverrides: profile.providerOverrides ?? {}, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }; await this.save(); } async get(name) { const data = await this.load(); return data.profiles[name]; } async list() { const data = await this.load(); return Object.values(data.profiles); } async edit(name, changes, serverRegistry) { const data = await this.load(); const existing = data.profiles[name]; if (!existing) throw new Error(`Profile "${name}" not found.`); const current = { ...existing, servers: [...existing.servers] }; if (changes.description !== undefined) current.description = changes.description; if (changes.addServers && changes.addServers.length > 0) { await this.validateServers(changes.addServers, serverRegistry); for (const s of changes.addServers) { if (!current.servers.includes(s)) current.servers.push(s); } } if (changes.removeServers && changes.removeServers.length > 0) { current.servers = current.servers.filter((s) => !(changes.removeServers ?? []).includes(s)); } current.updatedAt = new Date().toISOString(); data.profiles[name] = current; await this.save(); return data.profiles[name]; } async remove(name) { const data = await this.load(); if (!data.profiles[name]) throw new Error(`Profile "${name}" not found.`); delete data.profiles[name]; await this.save(); } async resolveServers(name, serverRegistry) { const profile = await this.get(name); if (!profile) throw new Error(`Profile "${name}" not found.`); if (profile.servers.includes('__all__') && serverRegistry) { return serverRegistry.list(); } if (!serverRegistry) return profile.servers; const resolved = []; for (const serverName of profile.servers) { const server = await serverRegistry.get(serverName); if (server) resolved.push(server); } return resolved; } async importFrom(filePath) { const content = await readFile(filePath, 'utf-8'); const imported = JSON.parse(content); const data = await this.load(); let added = 0; let updated = 0; const profiles = imported.profiles ?? {}; for (const [name, profile] of Object.entries(profiles)) { try { this.validateName(name); } catch { continue; } if (data.profiles[name]) { data.profiles[name] = { ...data.profiles[name], ...profile, name, updatedAt: new Date().toISOString(), }; updated++; } else { data.profiles[name] = { name, description: profile.description, servers: profile.servers ?? [], providerOverrides: profile.providerOverrides ?? {}, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }; added++; } } await this.save(); return { added, updated }; } async exportTo(filePath, profileName) { const data = await this.load(); if (profileName && !data.profiles[profileName]) { throw new Error(`Profile "${profileName}" not found.`); } const toExport = profileName ? { [profileName]: data.profiles[profileName] } : data.profiles; await writeFile(filePath, JSON.stringify({ profiles: toExport }, null, 2) + '\n', 'utf-8'); } async initPresets() { const data = await this.load(); let added = 0; for (const [name, preset] of Object.entries(PRESET_PROFILES)) { if (!data.profiles[name]) { data.profiles[name] = { name, ...preset, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }; added++; } } await this.save(); return { added, total: Object.keys(PRESET_PROFILES).length }; } clearCache() { this.cache = null; } } //# sourceMappingURL=profiles.js.map