#!/usr/bin/env node // kimi-eyes — sync the models.dev capability database into a slim local cache. // Run this when packaging/releasing the plugin: // // node scripts/sync-models.mjs // // Options: // --timeout curl timeout (default 180) // --out output file (default ./mcp/models-db.json) // --endpoint override the source URL (default https://models.dev/models.json) // // Proxy: the script uses the HTTPS_PROXY / https_proxy environment variable if set // (for users behind Clash/V2Ray etc.), falling back to a direct connection. import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import { execFileSync } from 'node:child_process'; const DEFAULT_ENDPOINT = 'https://models.dev/models.json'; const DEFAULT_OUT = path.join('mcp', 'models-db.json'); // Models known to support image input but missing from models.dev. // Maintained manually — merged into the db on every sync so re-syncs never drop them. const EXTRA_ENTRIES = { 'k3-256k': { name: 'K3-256k', image: true }, 'kimi-k3-256k': { name: 'K3-256k', image: true }, 'moonshot/k3-256k': { name: 'K3-256k', image: true }, 'kimi-for-coding': { name: 'Kimi For Coding', image: true }, 'moonshotai/kimi-for-coding': { name: 'Kimi For Coding', image: true }, 'kimi-for-coding-highspeed': { name: 'Kimi For Coding Highspeed', image: true }, 'moonshotai/kimi-for-coding-highspeed': { name: 'Kimi For Coding Highspeed', image: true }, }; function parseArgs(argv) { const args = { timeout: 180, out: DEFAULT_OUT, endpoint: DEFAULT_ENDPOINT }; for (let i = 0; i < argv.length; i++) { if (argv[i] === '--timeout') args.timeout = Number(argv[++i]) || 180; else if (argv[i] === '--out') args.out = argv[++i]; else if (argv[i] === '--endpoint') args.endpoint = argv[++i]; } return args; } function download(endpoint, outFile, timeout) { const proxy = process.env.HTTPS_PROXY || process.env.https_proxy; const curl = ['-sSL', '--max-time', String(timeout), '-o', outFile, endpoint]; const args = proxy ? ['-x', proxy, ...curl] : curl; try { execFileSync('curl', args, { stdio: 'inherit', timeout: (timeout + 30) * 1000 }); } catch (err) { throw new Error( `Failed to download ${endpoint}${proxy ? ` via proxy ${proxy}` : ''}. ` + `If you are behind a proxy, set HTTPS_PROXY (e.g. HTTPS_PROXY=http://127.0.0.1:7897). ` + `(${err.message.split('\n')[0]})` ); } } const args = parseArgs(process.argv.slice(2)); const tmpFile = path.join(os.tmpdir(), `models-dev-${Date.now()}.json`); console.log(`[sync-models] downloading ${args.endpoint} ...`); download(args.endpoint, tmpFile, args.timeout); const raw = JSON.parse(fs.readFileSync(tmpFile, 'utf8')); if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) { fs.rmSync(tmpFile, { force: true }); throw new Error(`Unexpected response shape from ${args.endpoint} (expected a flat model map).`); } // Slim down: keep only what the plugin needs. const slim = {}; let imageCount = 0; for (const [id, m] of Object.entries(raw)) { if (!m || typeof m !== 'object') continue; const input = Array.isArray(m.modalities?.input) ? m.modalities.input : []; const image = input.includes('image'); if (image) imageCount++; slim[id] = { name: typeof m.name === 'string' ? m.name : id, image }; } // Merge manual extras (they override the downloaded data on id collisions). for (const [id, m] of Object.entries(EXTRA_ENTRIES)) { slim[id] = m; if (m.image) imageCount++; } fs.rmSync(tmpFile, { force: true }); fs.mkdirSync(path.dirname(args.out), { recursive: true }); fs.writeFileSync(args.out, `${JSON.stringify(slim)}\n`, 'utf8'); const kb = (fs.statSync(args.out).size / 1024).toFixed(0); console.log( `[sync-models] wrote ${Object.keys(slim).length} models (${imageCount} support image input) to ${args.out} (${kb} KB)` );