322 lines
11 KiB
JavaScript
322 lines
11 KiB
JavaScript
// ChatGPT MCP Server — Local Setup Helper
|
|
// A simple interactive onboarding script. No external dependencies.
|
|
// Usage: npm run setup
|
|
|
|
import readline from "node:readline";
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import tty from "node:tty";
|
|
|
|
const SUPPORTED_PROVIDERS = ["openai", "manual", "ollama"];
|
|
|
|
const ROOT_DIR = path.resolve(import.meta.dirname, "..");
|
|
|
|
// ── Helpers ───────────────────────────────────────────────
|
|
|
|
function maskedInput(prompt) {
|
|
return new Promise((resolve) => {
|
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
|
|
// Detect if stdin is a TTY (interactive terminal).
|
|
const isTTY = process.stdin.isTTY && typeof process.stdin.setRawMode === "function";
|
|
|
|
if (!isTTY) {
|
|
console.log("\n⚠ Non-interactive terminal detected — input will be visible.");
|
|
}
|
|
|
|
rl.question(prompt + ": ", (answer) => resolve(answer));
|
|
});
|
|
}
|
|
|
|
function getProviderDefault() {
|
|
const envPath = path.join(ROOT_DIR, ".env");
|
|
if (fs.existsSync(envPath)) {
|
|
const content = fs.readFileSync(envPath, "utf-8");
|
|
const match = content.match(/^CHATGPT_MCP_PROVIDER\s*=\s*(\S+)/im);
|
|
if (match) return match[1];
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function promptProvider() {
|
|
const existing = getProviderDefault();
|
|
console.log("\nSelect a provider:");
|
|
console.log(" [1] openai — Automated second-opinion queries (requires API key)");
|
|
console.log(" [2] manual — Copy-paste prompts into ChatGPT Web (no API key needed)");
|
|
console.log(" [3] ollama — Local AI via Ollama (no API key needed)");
|
|
|
|
return new Promise((resolve) => {
|
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
const hint = existing && SUPPORTED_PROVIDERS.includes(existing) ? ` (default: ${existing})` : "";
|
|
|
|
rl.question(`\nProvider${hint}: `, (input) => {
|
|
rl.close();
|
|
|
|
// Allow default by empty input
|
|
if (!input.trim()) {
|
|
return resolve(existing || "openai");
|
|
}
|
|
|
|
const map = { 1: "openai", 2: "manual", 3: "ollama" };
|
|
const selected = map[input.trim()];
|
|
if (SUPPORTED_PROVIDERS.includes(selected)) {
|
|
return resolve(selected);
|
|
}
|
|
|
|
console.log(`\nInvalid selection: ${input.trim()}. Try again.\n`);
|
|
return resolve(promptProvider()); // retry
|
|
});
|
|
});
|
|
}
|
|
|
|
function promptRequired(label, defaultVal) {
|
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
|
|
return new Promise((resolve) => {
|
|
function ask() {
|
|
const hint = defaultVal ? ` (default: ${defaultVal})` : "";
|
|
rl.question(`${label}${hint}: `, (answer) => {
|
|
if (defaultVal && !answer.trim()) {
|
|
rl.close();
|
|
return resolve(defaultVal);
|
|
}
|
|
if (!answer.trim()) {
|
|
console.log(" This field is required. Please enter a value.");
|
|
return ask();
|
|
}
|
|
rl.close();
|
|
resolve(answer.trim());
|
|
});
|
|
}
|
|
ask();
|
|
});
|
|
}
|
|
|
|
function promptOptional(label, defaultVal) {
|
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
|
|
return new Promise((resolve) => {
|
|
const hint = defaultVal ? ` (default: ${defaultVal})` : "";
|
|
rl.question(`${label}${hint}: `, (answer) => {
|
|
rl.close();
|
|
if (!answer.trim() && defaultVal) {
|
|
return resolve(defaultVal);
|
|
}
|
|
return resolve(answer.trim());
|
|
});
|
|
});
|
|
}
|
|
|
|
function promptConfirm(message) {
|
|
return new Promise((resolve) => {
|
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
rl.question(`${message} [y/N]: `, (answer) => {
|
|
rl.close();
|
|
resolve(["y", "yes"].includes(answer.trim().toLowerCase()));
|
|
});
|
|
});
|
|
}
|
|
|
|
// ── .env generation ───────────────────────────────────────
|
|
|
|
function buildEnvContent(provider, config) {
|
|
const lines = [];
|
|
|
|
// Provider section header
|
|
lines.push(`# Chat provider: "openai", "manual", or "ollama"`);
|
|
lines.push(`CHATGPT_MCP_PROVIDER=${provider}`);
|
|
lines.push("");
|
|
|
|
if (provider === "openai") {
|
|
lines.push("# OpenAI provider settings");
|
|
lines.push(`OPENAI_API_KEY=${config.openaiApiKey || ""}`);
|
|
if (config.openaiModel) lines.push(`OPENAI_MODEL=${config.openaiModel}`);
|
|
lines.push("");
|
|
}
|
|
|
|
if (provider === "ollama") {
|
|
lines.push("# Ollama provider settings");
|
|
if (config.ollamaBaseUrl) lines.push(`OLLAMA_BASE_URL=${config.ollamaBaseUrl}`);
|
|
if (config.ollamaModel) lines.push(`OLLAMA_MODEL=${config.ollamaModel}`);
|
|
if (config.ollamaTemperature !== undefined && config.ollamaTemperature !== null)
|
|
lines.push(`OLLAMA_TEMPERATURE=${config.ollamaTemperature}`);
|
|
if (config.ollamaTimeout !== undefined && config.ollamaTimeout !== null)
|
|
lines.push(`OLLAMA_TIMEOUT=${config.ollamaTimeout}`);
|
|
lines.push("");
|
|
}
|
|
|
|
// Append existing .env content that doesn't conflict with our keys.
|
|
const envPath = path.join(ROOT_DIR, ".env");
|
|
if (fs.existsSync(envPath)) {
|
|
const existingContent = fs.readFileSync(envPath, "utf-8");
|
|
const existingKeys = new Set([
|
|
"CHATGPT_MCP_PROVIDER",
|
|
"OPENAI_API_KEY",
|
|
"OPENAI_MODEL",
|
|
"OLLAMA_BASE_URL",
|
|
"OLLAMA_MODEL",
|
|
"OLLAMA_TEMPERATURE",
|
|
"OLLAMA_TIMEOUT",
|
|
]);
|
|
|
|
const otherLines = existingContent
|
|
.split("\n")
|
|
.filter((line) => {
|
|
if (line.trim().startsWith("#")) return true;
|
|
const keyMatch = line.match(/^(\w+)/);
|
|
if (!keyMatch) return true;
|
|
return !existingKeys.has(keyMatch[1]);
|
|
})
|
|
// Deduplicate: keep last occurrence of each non-comment line
|
|
.reduceRight((acc, line) => {
|
|
const trimmed = line.trim();
|
|
if (trimmed && acc.includes(trimmed)) return acc;
|
|
if (trimmed === "") return acc;
|
|
return [line, ...acc];
|
|
}, []);
|
|
|
|
if (otherLines.length > 0) {
|
|
lines.push("# ── Other settings (preserved from existing .env) ──");
|
|
lines.push(...otherLines);
|
|
}
|
|
}
|
|
|
|
// Ensure trailing newline
|
|
return lines.join("\n") + "\n";
|
|
}
|
|
|
|
// ── Claude config generation ──────────────────────────────
|
|
|
|
function buildClaudeConfig() {
|
|
return JSON.stringify(
|
|
{
|
|
mcpServers: {
|
|
"chatgpt-mcp": {
|
|
command: "npm",
|
|
args: ["start"],
|
|
},
|
|
},
|
|
},
|
|
null,
|
|
2,
|
|
);
|
|
}
|
|
|
|
// ── Exports for testing ─────────────────────────────────
|
|
|
|
export { SUPPORTED_PROVIDERS, buildEnvContent, buildClaudeConfig };
|
|
|
|
// ── Main flow ─────────────────────────────────────────────
|
|
|
|
async function main() {
|
|
console.log("\n╔══════════════════════════════════════════════╗");
|
|
console.log("║ ChatGPT MCP Server — Local Setup Helper ║");
|
|
console.log("╚══════════════════════════════════════════════╝\n");
|
|
|
|
// Step 1: Provider selection
|
|
const provider = await promptProvider();
|
|
console.log(`\n✓ Selected provider: ${provider}`);
|
|
|
|
// Step 2: Provider-specific config prompts
|
|
let config;
|
|
if (provider === "openai") {
|
|
config = { openaiApiKey: "", openaiModel: null };
|
|
// Mask the API key input by disabling terminal echo temporarily.
|
|
const isTTY = process.stdin.isTTY && typeof process.stdin.setRawMode === "function";
|
|
if (isTTY) {
|
|
process.stdin.setRawMode(true);
|
|
process.stdout.write("\n");
|
|
}
|
|
|
|
const rlKey = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
config.openaiApiKey = await new Promise((resolve) => {
|
|
console.log("\nOPENAI_API_KEY (required — not shown as you type):");
|
|
rlKey.question(" Key: ", resolve);
|
|
});
|
|
|
|
if (isTTY) process.stdin.setRawMode(false);
|
|
rlKey.close();
|
|
process.stdout.write("\n"); // newline after hidden input
|
|
|
|
config.openaiModel = await promptOptional("OPENAI_MODEL", "gpt-5.1");
|
|
}
|
|
|
|
if (provider === "ollama") {
|
|
config = {};
|
|
config.ollamaBaseUrl = await promptOptional("OLLAMA_BASE_URL", "http://localhost:11434");
|
|
config.ollamaModel = await promptRequired("OLLAMA_MODEL", "qwen3.6:35b-a3b");
|
|
config.ollamaTemperature = await promptOptional("OLLAMA_TEMPERATURE", "0.2");
|
|
config.ollamaTimeout = await promptOptional("OLLAMA_TIMEOUT", "60");
|
|
}
|
|
|
|
// manual provider — no prompts needed (config is empty object)
|
|
if (provider === "manual") {
|
|
config = {};
|
|
}
|
|
|
|
// Step 3: Generate and confirm .env
|
|
const envContent = buildEnvContent(provider, config);
|
|
console.log("\n" + "─".repeat(52));
|
|
console.log("The following will be written to .env:\n");
|
|
process.stdout.write(envContent);
|
|
console.log("─".repeat(52) + "\n");
|
|
|
|
const confirmEnv = await promptConfirm("Overwrite .env with this configuration?");
|
|
if (!confirmEnv) {
|
|
console.log("\n✗ Setup cancelled. No changes were made.");
|
|
process.exit(0);
|
|
}
|
|
|
|
const envPath = path.join(ROOT_DIR, ".env");
|
|
fs.writeFileSync(envPath, envContent, "utf-8");
|
|
console.log("✓ .env updated.\n");
|
|
|
|
// Step 4: Optionally create Claude Code config
|
|
const claudeDir = path.join(ROOT_DIR, ".claude");
|
|
const claudeConfigPath = path.join(claudeDir, "settings.local.json");
|
|
|
|
let confirmClaude = false;
|
|
if (fs.existsSync(claudeConfigPath)) {
|
|
console.log(`⚠ ${claudeConfigPath} already exists.`);
|
|
confirmClaude = await promptConfirm("Overwrite it?");
|
|
} else {
|
|
console.log(".claude/ directory not found — will be created.");
|
|
// Check if .claude dir itself exists but settings doesn't
|
|
if (!fs.existsSync(claudeDir)) {
|
|
console.log("Creating .claude/ directory...");
|
|
fs.mkdirSync(claudeDir, { recursive: true });
|
|
}
|
|
confirmClaude = await promptConfirm("Create .claude/settings.local.json?");
|
|
}
|
|
|
|
if (confirmClaude) {
|
|
if (!fs.existsSync(claudeDir)) {
|
|
fs.mkdirSync(claudeDir, { recursive: true });
|
|
}
|
|
fs.writeFileSync(claudeConfigPath, buildClaudeConfig() + "\n", "utf-8");
|
|
console.log("✓ .claude/settings.local.json created.\n");
|
|
} else {
|
|
console.log(".claude/settings.local.json was skipped.\n");
|
|
}
|
|
|
|
// Done
|
|
console.log("╔══════════════════════════════════════════════╗");
|
|
console.log("║ Setup complete! ║");
|
|
console.log("╚══════════════════════════════════════════════╝\n");
|
|
console.log("Next steps:");
|
|
console.log(" npm test — Run the test suite");
|
|
console.log(" npm start — Start the MCP server");
|
|
console.log(` CHATGPT_MCP_PROVIDER=${provider} (active provider)`);
|
|
console.log("");
|
|
|
|
if (provider === "ollama") {
|
|
console.log("Tip: verify your Ollama setup with: ollama list");
|
|
console.log("");
|
|
}
|
|
}
|
|
|
|
main().catch((err) => {
|
|
console.error(`Setup failed: ${err.message}`);
|
|
process.exit(1);
|
|
});
|