feat: file cache, pause queue, hot-reload, and cache CLI commands
- Persistent file cache in ~/.mcpctl/cache/proxymodel/ with LRU eviction - Pause queue for temporarily holding MCP traffic - Hot-reload watcher for custom stages and proxymodel definitions - CLI: mcpctl cache list/clear/stats commands - HTTP endpoints for cache and pause management Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
137
src/cli/src/commands/cache.ts
Normal file
137
src/cli/src/commands/cache.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
import { Command } from 'commander';
|
||||
import http from 'node:http';
|
||||
|
||||
export interface CacheCommandDeps {
|
||||
log: (...args: string[]) => void;
|
||||
mcplocalUrl?: string;
|
||||
}
|
||||
|
||||
interface NamespaceStats {
|
||||
name: string;
|
||||
entries: number;
|
||||
size: number;
|
||||
oldestMs: number;
|
||||
newestMs: number;
|
||||
}
|
||||
|
||||
interface CacheStats {
|
||||
rootDir: string;
|
||||
totalSize: number;
|
||||
totalEntries: number;
|
||||
namespaces: NamespaceStats[];
|
||||
}
|
||||
|
||||
interface ClearResult {
|
||||
removed: number;
|
||||
freedBytes: number;
|
||||
}
|
||||
|
||||
function formatBytes(bytes: number): string {
|
||||
if (bytes === 0) return '0 B';
|
||||
const units = ['B', 'KB', 'MB', 'GB'];
|
||||
const i = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1);
|
||||
const val = bytes / Math.pow(1024, i);
|
||||
return `${val < 10 ? val.toFixed(1) : Math.round(val)} ${units[i]}`;
|
||||
}
|
||||
|
||||
function formatAge(ms: number): string {
|
||||
if (ms === 0) return '-';
|
||||
const age = Date.now() - ms;
|
||||
const days = Math.floor(age / (24 * 60 * 60 * 1000));
|
||||
if (days > 0) return `${days}d ago`;
|
||||
const hours = Math.floor(age / (60 * 60 * 1000));
|
||||
if (hours > 0) return `${hours}h ago`;
|
||||
const mins = Math.floor(age / (60 * 1000));
|
||||
return `${mins}m ago`;
|
||||
}
|
||||
|
||||
function fetchJson<T>(url: string, method = 'GET'): Promise<T> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = http.request(url, { method, timeout: 5000 }, (res) => {
|
||||
let data = '';
|
||||
res.on('data', (chunk: Buffer) => { data += chunk.toString(); });
|
||||
res.on('end', () => {
|
||||
try {
|
||||
resolve(JSON.parse(data) as T);
|
||||
} catch {
|
||||
reject(new Error(`Invalid response from mcplocal: ${data.slice(0, 200)}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
req.on('error', () => reject(new Error('Cannot connect to mcplocal. Is it running?')));
|
||||
req.on('timeout', () => { req.destroy(); reject(new Error('mcplocal request timed out')); });
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
export function createCacheCommand(deps: CacheCommandDeps): Command {
|
||||
const cache = new Command('cache')
|
||||
.description('Manage ProxyModel pipeline cache');
|
||||
|
||||
const mcplocalUrl = deps.mcplocalUrl ?? 'http://localhost:3200';
|
||||
|
||||
cache
|
||||
.command('stats')
|
||||
.description('Show cache statistics')
|
||||
.action(async () => {
|
||||
const stats = await fetchJson<CacheStats>(`${mcplocalUrl}/cache/stats`);
|
||||
|
||||
if (stats.totalEntries === 0) {
|
||||
deps.log('Cache is empty.');
|
||||
return;
|
||||
}
|
||||
|
||||
deps.log(`Cache: ${formatBytes(stats.totalSize)} total, ${stats.totalEntries} entries`);
|
||||
deps.log(`Path: ${stats.rootDir}`);
|
||||
deps.log('');
|
||||
|
||||
// Table header
|
||||
const pad = (s: string, w: number) => s.padEnd(w);
|
||||
deps.log(
|
||||
`${pad('NAMESPACE', 40)} ${pad('ENTRIES', 8)} ${pad('SIZE', 10)} ${pad('OLDEST', 12)} NEWEST`,
|
||||
);
|
||||
deps.log(
|
||||
`${pad('-'.repeat(40), 40)} ${pad('-'.repeat(8), 8)} ${pad('-'.repeat(10), 10)} ${pad('-'.repeat(12), 12)} ${'-'.repeat(12)}`,
|
||||
);
|
||||
|
||||
for (const ns of stats.namespaces) {
|
||||
deps.log(
|
||||
`${pad(ns.name, 40)} ${pad(String(ns.entries), 8)} ${pad(formatBytes(ns.size), 10)} ${pad(formatAge(ns.oldestMs), 12)} ${formatAge(ns.newestMs)}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
cache
|
||||
.command('clear')
|
||||
.description('Clear cache entries')
|
||||
.argument('[namespace]', 'Clear only this namespace')
|
||||
.option('--older-than <days>', 'Clear entries older than N days')
|
||||
.option('-y, --yes', 'Skip confirmation')
|
||||
.action(async (namespace: string | undefined, opts: { olderThan?: string; yes?: boolean }) => {
|
||||
// Show what will be cleared first
|
||||
const stats = await fetchJson<CacheStats>(`${mcplocalUrl}/cache/stats`);
|
||||
if (stats.totalEntries === 0) {
|
||||
deps.log('Cache is already empty.');
|
||||
return;
|
||||
}
|
||||
|
||||
const target = namespace
|
||||
? stats.namespaces.find((ns) => ns.name === namespace)
|
||||
: null;
|
||||
if (namespace && !target) {
|
||||
deps.log(`Namespace '${namespace}' not found.`);
|
||||
deps.log(`Available: ${stats.namespaces.map((ns) => ns.name).join(', ')}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const olderThan = opts.olderThan ? `?olderThan=${opts.olderThan}` : '';
|
||||
const url = namespace
|
||||
? `${mcplocalUrl}/cache/${encodeURIComponent(namespace)}${olderThan}`
|
||||
: `${mcplocalUrl}/cache${olderThan}`;
|
||||
|
||||
const result = await fetchJson<ClearResult>(url, 'DELETE');
|
||||
deps.log(`Cleared ${result.removed} entries, freed ${formatBytes(result.freedBytes)}`);
|
||||
});
|
||||
|
||||
return cache;
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import { createAttachServerCommand, createDetachServerCommand, createApproveComm
|
||||
import { createMcpCommand } from './commands/mcp.js';
|
||||
import { createPatchCommand } from './commands/patch.js';
|
||||
import { createConsoleCommand } from './commands/console/index.js';
|
||||
import { createCacheCommand } from './commands/cache.js';
|
||||
import { ApiClient, ApiError } from './api-client.js';
|
||||
import { loadConfig } from './config/index.js';
|
||||
import { loadCredentials } from './auth/index.js';
|
||||
@@ -211,6 +212,11 @@ export function createProgram(): Command {
|
||||
getProject: () => program.opts().project as string | undefined,
|
||||
}));
|
||||
|
||||
program.addCommand(createCacheCommand({
|
||||
log: (...args) => console.log(...args),
|
||||
mcplocalUrl: config.mcplocalUrl,
|
||||
}));
|
||||
|
||||
return program;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user