162 lines
5.2 KiB
JavaScript
162 lines
5.2 KiB
JavaScript
|
|
"use strict";
|
||
|
|
|
||
|
|
const os = require("node:os");
|
||
|
|
const path = require("node:path");
|
||
|
|
const fs = require("node:fs");
|
||
|
|
|
||
|
|
const EXTERNAL_MCP_STATE_DIR_NAME = "external-mcp";
|
||
|
|
const EXTERNAL_MCP_DISCOVERY_ENV_VAR = "NETCATTY_EXTERNAL_MCP_DISCOVERY_FILE";
|
||
|
|
const EXTERNAL_MCP_CHAT_SESSION_ID = "__external_mcp__";
|
||
|
|
const FALLBACK_APP_DATA_DIR_NAME = "Netcatty";
|
||
|
|
|
||
|
|
function toUnpackedAsarPath(filePath) {
|
||
|
|
return filePath.replace(/app\.asar([\\/])/, "app.asar.unpacked$1");
|
||
|
|
}
|
||
|
|
|
||
|
|
function getDefaultAppDataDirName(options = {}) {
|
||
|
|
const packageJsonPaths = Array.isArray(options.packageJsonPaths) && options.packageJsonPaths.length > 0
|
||
|
|
? options.packageJsonPaths
|
||
|
|
: [
|
||
|
|
process.resourcesPath ? path.join(process.resourcesPath, "app.asar", "package.json") : null,
|
||
|
|
path.resolve(__dirname, "../../package.json"),
|
||
|
|
path.join(process.cwd(), "package.json"),
|
||
|
|
].filter(Boolean);
|
||
|
|
|
||
|
|
for (const packageJsonPath of packageJsonPaths) {
|
||
|
|
try {
|
||
|
|
const packageJson = require(packageJsonPath);
|
||
|
|
if (typeof packageJson?.productName === "string" && packageJson.productName) {
|
||
|
|
return packageJson.productName;
|
||
|
|
}
|
||
|
|
} catch {
|
||
|
|
// Try next candidate.
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Prefer Electron productName casing over package.json "name" (netcatty).
|
||
|
|
return FALLBACK_APP_DATA_DIR_NAME;
|
||
|
|
}
|
||
|
|
|
||
|
|
function getPlatformUserDataRoot(appDataDirName) {
|
||
|
|
if (process.platform === "darwin") {
|
||
|
|
return path.join(os.homedir(), "Library", "Application Support", appDataDirName);
|
||
|
|
}
|
||
|
|
if (process.platform === "win32") {
|
||
|
|
const appData = process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming");
|
||
|
|
return path.join(appData, appDataDirName);
|
||
|
|
}
|
||
|
|
const xdgConfigHome = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config");
|
||
|
|
return path.join(xdgConfigHome, appDataDirName);
|
||
|
|
}
|
||
|
|
|
||
|
|
function getDefaultUserDataDir() {
|
||
|
|
return getPlatformUserDataRoot(getDefaultAppDataDirName());
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Candidate userData roots the launcher may need when env is missing.
|
||
|
|
* Includes packaged (Netcatty), lowercase package name, and Electron Dev (/dev).
|
||
|
|
*/
|
||
|
|
function listCandidateUserDataDirs(options = {}) {
|
||
|
|
const names = Array.from(new Set([
|
||
|
|
getDefaultAppDataDirName(options),
|
||
|
|
"Netcatty",
|
||
|
|
"netcatty",
|
||
|
|
"Netcatty Dev",
|
||
|
|
].filter(Boolean)));
|
||
|
|
const roots = [];
|
||
|
|
for (const name of names) {
|
||
|
|
const root = getPlatformUserDataRoot(name);
|
||
|
|
roots.push(root);
|
||
|
|
roots.push(path.join(root, "dev"));
|
||
|
|
}
|
||
|
|
return Array.from(new Set(roots));
|
||
|
|
}
|
||
|
|
|
||
|
|
function getConfiguredDiscoveryFilePath() {
|
||
|
|
return process.env[EXTERNAL_MCP_DISCOVERY_ENV_VAR] || null;
|
||
|
|
}
|
||
|
|
|
||
|
|
function getExternalMcpStateDir(options = {}) {
|
||
|
|
const discoveryFilePath = getConfiguredDiscoveryFilePath();
|
||
|
|
if (discoveryFilePath) {
|
||
|
|
return path.dirname(discoveryFilePath);
|
||
|
|
}
|
||
|
|
const userDataDir = typeof options.userDataDir === "string" && options.userDataDir
|
||
|
|
? options.userDataDir
|
||
|
|
: getDefaultUserDataDir();
|
||
|
|
return path.join(userDataDir, EXTERNAL_MCP_STATE_DIR_NAME);
|
||
|
|
}
|
||
|
|
|
||
|
|
function getExternalMcpDiscoveryFilePath(options = {}) {
|
||
|
|
const discoveryFilePath = getConfiguredDiscoveryFilePath();
|
||
|
|
if (discoveryFilePath) {
|
||
|
|
return discoveryFilePath;
|
||
|
|
}
|
||
|
|
return path.join(getExternalMcpStateDir(options), "discovery.json");
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Resolve an existing discovery file for launcher/bootstrap use.
|
||
|
|
* Prefers the env override, then the default path, then common Electron userData variants.
|
||
|
|
*/
|
||
|
|
function resolveExistingExternalMcpDiscoveryFilePath(options = {}) {
|
||
|
|
const configured = getConfiguredDiscoveryFilePath();
|
||
|
|
// Explicit client env must not silently fall back to another profile's file.
|
||
|
|
if (configured) {
|
||
|
|
return configured;
|
||
|
|
}
|
||
|
|
|
||
|
|
const primary = getExternalMcpDiscoveryFilePath(
|
||
|
|
options.userDataDir ? { userDataDir: options.userDataDir } : {},
|
||
|
|
);
|
||
|
|
if (fs.existsSync(primary)) {
|
||
|
|
return primary;
|
||
|
|
}
|
||
|
|
|
||
|
|
for (const userDataDir of listCandidateUserDataDirs(options)) {
|
||
|
|
const candidate = path.join(userDataDir, EXTERNAL_MCP_STATE_DIR_NAME, "discovery.json");
|
||
|
|
if (candidate !== primary && fs.existsSync(candidate)) {
|
||
|
|
return candidate;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return primary;
|
||
|
|
}
|
||
|
|
|
||
|
|
function getExternalMcpLauncherPath() {
|
||
|
|
const fileName = process.platform === "win32"
|
||
|
|
? "netcatty-external-mcp.cmd"
|
||
|
|
: "netcatty-external-mcp";
|
||
|
|
return toUnpackedAsarPath(path.join(__dirname, fileName));
|
||
|
|
}
|
||
|
|
|
||
|
|
function buildDiscoveryEnv(discoveryFilePath) {
|
||
|
|
if (!discoveryFilePath) return {};
|
||
|
|
return { [EXTERNAL_MCP_DISCOVERY_ENV_VAR]: discoveryFilePath };
|
||
|
|
}
|
||
|
|
|
||
|
|
function formatDiscoveryEnvCliFlags(discoveryEnv, style = "codex") {
|
||
|
|
const entries = Object.entries(discoveryEnv || {}).filter(([, value]) => typeof value === "string" && value);
|
||
|
|
if (entries.length === 0) return [];
|
||
|
|
if (style === "claude" || style === "grok") {
|
||
|
|
return entries.flatMap(([key, value]) => ["-e", `${key}=${value}`]);
|
||
|
|
}
|
||
|
|
// Codex: --env KEY=VALUE
|
||
|
|
return entries.flatMap(([key, value]) => ["--env", `${key}=${value}`]);
|
||
|
|
}
|
||
|
|
|
||
|
|
module.exports = {
|
||
|
|
getDefaultAppDataDirName,
|
||
|
|
getExternalMcpStateDir,
|
||
|
|
getExternalMcpDiscoveryFilePath,
|
||
|
|
resolveExistingExternalMcpDiscoveryFilePath,
|
||
|
|
getExternalMcpLauncherPath,
|
||
|
|
listCandidateUserDataDirs,
|
||
|
|
buildDiscoveryEnv,
|
||
|
|
formatDiscoveryEnvCliFlags,
|
||
|
|
EXTERNAL_MCP_DISCOVERY_ENV_VAR,
|
||
|
|
EXTERNAL_MCP_CHAT_SESSION_ID,
|
||
|
|
EXTERNAL_MCP_STATE_DIR_NAME,
|
||
|
|
};
|