refactor: clis, config-generator, readmes (#4)

This commit is contained in:
gzuuus
2025-03-01 15:38:13 +01:00
committed by GitHub
parent db0da6909c
commit a75fb9de1c
12 changed files with 556 additions and 600 deletions

View File

@@ -1,230 +1,127 @@
#!/usr/bin/env bun
import { randomBytes } from 'node:crypto';
import { createInterface } from 'node:readline';
import { writeFileSync, existsSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { parse, stringify } from 'yaml';
import type { Config } from './src/types.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const isNpxRun = !__dirname.includes(process.cwd());
if (!isNpxRun) {
process.chdir(__dirname);
}
import { existsSync } from 'node:fs';
import { join } from 'path';
import {
ConfigGenerator,
type FieldConfig,
} from '@dvmcp/commons/config-generator';
import {
generateHexKey,
validateHexKey,
validateRelayUrl,
CONFIG_EMOJIS,
} from '@dvmcp/commons/config-generator';
import { argv } from 'process';
import type { Config } from './src/types';
const configPath = join(process.cwd(), 'config.yml');
const configExamplePath = join(__dirname, 'config.example.yml');
async function setupConfig() {
console.log('🔧 DVMCP Bridge Configuration Setup 🔧');
const configFields: Record<string, FieldConfig> = {
nostr: {
type: 'nested',
description: 'Nostr Configuration',
emoji: CONFIG_EMOJIS.NOSTR,
fields: {
privateKey: {
type: 'hex',
description: 'Private key',
generator: generateHexKey,
validation: validateHexKey,
required: true,
},
relayUrls: {
type: 'array',
description: 'Relay URLs',
validation: validateRelayUrl,
required: true,
},
},
},
mcp: {
type: 'nested',
description: 'Service Configuration',
emoji: CONFIG_EMOJIS.SERVICE,
fields: {
name: {
type: 'string',
description: 'Service name',
default: 'DVM MCP Bridge',
},
about: {
type: 'string',
description: 'Service description',
default: 'MCP-enabled DVM providing AI and computational tools',
},
clientName: {
type: 'string',
description: 'Client name',
default: 'DVM MCP Bridge Client',
required: true,
},
clientVersion: {
type: 'string',
description: 'Client version',
default: '1.0.0',
required: true,
},
servers: {
type: 'object-array',
description: 'Server Configuration',
emoji: CONFIG_EMOJIS.SERVER,
required: true,
fields: {
command: {
type: 'string',
description: 'Command',
},
args: {
type: 'array',
description: 'Arguments',
},
},
},
},
},
whitelist: {
type: 'nested',
description: 'Whitelist Configuration',
emoji: CONFIG_EMOJIS.WHITELIST,
fields: {
allowedPubkeys: {
type: 'set',
description: 'Allowed public keys',
},
},
},
};
// If config exists, ask user if they want to reconfigure
if (existsSync(configPath)) {
const shouldReconfigure = await promptYesNo(
'Configuration file already exists. Do you want to reconfigure it?'
const configure = async () => {
console.log(
`${CONFIG_EMOJIS.SETUP} DVMCP Bridge Configuration Setup ${CONFIG_EMOJIS.SETUP}`
);
const generator = new ConfigGenerator<Config>(configPath, configFields);
await generator.generate();
};
const runApp = async () => {
const main = await import('./index.js');
console.log(`${CONFIG_EMOJIS.INFO} Running main application...`);
await main.default();
};
const cliMain = async () => {
if (argv.includes('--configure')) {
await configure();
}
if (!existsSync(configPath)) {
console.log(
`${CONFIG_EMOJIS.INFO} No configuration file found. Starting setup...`
);
if (!shouldReconfigure) {
console.log('Using existing configuration.');
return;
}
await configure();
}
// Load example config as template
if (!existsSync(configExamplePath)) {
console.error('Error: Example configuration file not found!');
process.exit(1);
}
await runApp();
};
// Read and parse example config
const exampleConfigContent = await Bun.file(configExamplePath).text();
const config: Config = parse(exampleConfigContent);
// Ensure config structure matches example
config.nostr = config.nostr || {};
config.mcp = config.mcp || {};
config.mcp.servers = config.mcp.servers || [];
console.log('\n🔑 Nostr Configuration:');
const useExistingKey = await promptYesNo(
'Do you have an existing Nostr private key?'
);
if (useExistingKey) {
config.nostr.privateKey = await prompt(
'Enter your Nostr private key (nsec or hex):',
config.nostr.privateKey || ''
);
} else {
// Generate a random key
config.nostr.privateKey = Buffer.from(randomBytes(32)).toString('hex');
console.log(`Generated new private key: ${config.nostr.privateKey}`);
}
console.log('\n🔄 Relay Configuration:');
let relayUrls = config.nostr.relayUrls || [];
console.log('Current relays:');
if (relayUrls.length > 0) {
relayUrls.forEach((relay, i) => console.log(` ${i + 1}. ${relay}`));
} else {
console.log(' No relays configured yet.');
}
const addRelays = await promptYesNo('Would you like to add more relays?');
if (addRelays) {
let addingRelays = true;
while (addingRelays) {
const relay = await prompt(
'Enter relay URL (or leave empty to finish):',
''
);
if (relay) {
relayUrls.push(relay);
} else {
addingRelays = false;
}
}
}
config.nostr.relayUrls = relayUrls;
console.log('\n🌐 MCP Service Configuration:');
config.mcp.name = await prompt(
'Service name:',
config.mcp.name || 'DVM MCP Bridge'
);
config.mcp.about = await prompt(
'Service description:',
config.mcp.about || 'MCP-enabled DVM providing AI and computational tools'
);
config.mcp.clientName = await prompt(
'Client name:',
config.mcp.clientName || 'DVM MCP Bridge Client'
);
config.mcp.clientVersion = await prompt(
'Client version:',
config.mcp.clientVersion || '1.0.0'
);
console.log('\n🖥 MCP Servers Configuration:');
console.log('Current configured servers:');
if (config.mcp.servers.length > 0) {
config.mcp.servers.forEach((server, i) => {
console.log(
` ${i + 1}. ${server.name} (${server.command} ${server.args.join(' ')})`
);
});
} else {
console.log(' No servers configured yet.');
}
const configureServers = await promptYesNo(
'Would you like to configure MCP servers?'
);
if (configureServers) {
let configuringServers = true;
while (configuringServers) {
console.log('\nConfiguring a new server:');
const name = await prompt('Server name (or leave empty to finish):', '');
if (!name) {
configuringServers = false;
continue;
}
const command = await prompt('Command to run server:', 'node');
const argsStr = await prompt('Command arguments (space-separated):', '');
const args = argsStr ? argsStr.split(' ') : [];
config.mcp.servers.push({ name, command, args });
}
}
console.log('\n📝 Whitelist Configuration:');
const useWhitelist = await promptYesNo(
'Would you like to configure a public key whitelist?'
);
if (useWhitelist) {
config.whitelist = config.whitelist || {};
config.whitelist.allowedPubkeys = config.whitelist.allowedPubkeys;
console.log('Current allowed public keys:');
if (
config.whitelist.allowedPubkeys &&
config.whitelist.allowedPubkeys.size > 0
) {
config.whitelist.allowedPubkeys.forEach((pubkey, i) =>
console.log(` ${i + 1}. ${pubkey}`)
);
} else {
console.log(' No public keys whitelisted yet.');
}
let addingPubkeys = true;
while (addingPubkeys) {
const pubkey = await prompt(
'Enter public key to whitelist (or leave empty to finish):',
''
);
if (pubkey) {
if (!config.whitelist.allowedPubkeys) {
config.whitelist.allowedPubkeys = new Set<string>();
}
config.whitelist.allowedPubkeys.add(pubkey);
} else {
addingPubkeys = false;
}
}
} else if (config.whitelist) {
// If user doesn't want a whitelist but it exists in config, remove it
config.whitelist.allowedPubkeys = undefined;
}
// Save the config
writeFileSync(configPath, stringify(config));
console.log(`\n✅ Configuration saved to ${configPath}`);
}
async function prompt(question: string, defaultValue = ''): Promise<string> {
const rl = createInterface({
input: process.stdin,
output: process.stdout,
});
return new Promise((resolve) => {
rl.question(
`${question}${defaultValue ? ` (${defaultValue})` : ''} `,
(answer) => {
rl.close();
resolve(answer || defaultValue);
}
);
});
}
async function promptYesNo(
question: string,
defaultValue = false
): Promise<boolean> {
const rl = createInterface({
input: process.stdin,
output: process.stdout,
});
const defaultIndicator = defaultValue ? 'Y/n' : 'y/N';
return new Promise((resolve) => {
rl.question(`${question} (${defaultIndicator}) `, (answer) => {
rl.close();
if (answer.trim() === '') {
resolve(defaultValue);
} else {
resolve(answer.toLowerCase().startsWith('y'));
}
});
});
}
await setupConfig();
import('./index.js');
cliMain().catch(console.error);