local-mcp 3.0.137 → 3.0.139
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/download.js +97 -31
- package/index.js +36 -16
- package/package.json +4 -2
- package/setup.js +117 -12
package/download.js
CHANGED
|
@@ -13,34 +13,61 @@ const os = require('os')
|
|
|
13
13
|
const { execFileSync } = require('child_process')
|
|
14
14
|
|
|
15
15
|
const BACKEND_URL = 'https://office-mcp-production.up.railway.app'
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
|
|
17
|
+
// Platform-aware cache directory:
|
|
18
|
+
// macOS: ~/.local/share/local-mcp/bin
|
|
19
|
+
// Windows: %LOCALAPPDATA%/local-mcp/bin
|
|
20
|
+
// Linux: ~/.local/share/local-mcp/bin
|
|
21
|
+
const CACHE_DIR = process.platform === 'win32'
|
|
22
|
+
? path.join(process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local'), 'local-mcp', 'bin')
|
|
23
|
+
: path.join(os.homedir(), '.local', 'share', 'local-mcp', 'bin')
|
|
24
|
+
|
|
25
|
+
const TRAY_DIR = process.platform === 'win32'
|
|
26
|
+
? path.join(process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local'), 'local-mcp', 'tray')
|
|
27
|
+
: path.join(os.homedir(), '.local', 'share', 'local-mcp', 'tray')
|
|
28
|
+
|
|
18
29
|
// Install to ~/Applications (user-owned) to avoid macOS App Management TCC prompt.
|
|
19
30
|
// /Applications requires kTCCServiceAppManagement permission which shows "node would like
|
|
20
31
|
// to access data from other apps" on every tray update — confusing and unnecessary.
|
|
21
|
-
|
|
32
|
+
// On Windows/Linux there is no tray app yet.
|
|
33
|
+
const TRAY_APP = process.platform === 'darwin'
|
|
34
|
+
? path.join(os.homedir(), 'Applications', 'LocalMCPTray.app')
|
|
35
|
+
: null
|
|
22
36
|
|
|
23
37
|
function _getMachineId() {
|
|
24
38
|
const { execSync } = require('child_process')
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
39
|
+
if (process.platform === 'darwin') {
|
|
40
|
+
try {
|
|
41
|
+
const r = execSync('security find-generic-password -s com.local-mcp.machine-id -a machine-id -w 2>/dev/null', { stdio: 'pipe' })
|
|
42
|
+
const id = r.toString().trim()
|
|
43
|
+
if (id) return id
|
|
44
|
+
} catch {}
|
|
45
|
+
try {
|
|
46
|
+
const ioreg = execSync('ioreg -rd1 -c IOPlatformExpertDevice', { stdio: 'pipe' }).toString()
|
|
47
|
+
const match = ioreg.match(/"IOPlatformUUID"\s*=\s*"([^"]+)"/)
|
|
48
|
+
return match ? match[1] : os.hostname()
|
|
49
|
+
} catch {}
|
|
50
|
+
} else if (process.platform === 'win32') {
|
|
51
|
+
try {
|
|
52
|
+
const wmic = execSync('wmic csproduct get UUID /value', { stdio: 'pipe' }).toString()
|
|
53
|
+
const match = wmic.match(/UUID=(.+)/)
|
|
54
|
+
if (match) return match[1].trim()
|
|
55
|
+
} catch {}
|
|
56
|
+
} else {
|
|
57
|
+
try {
|
|
58
|
+
const mid = fs.readFileSync('/etc/machine-id', 'utf8').trim()
|
|
59
|
+
if (mid) return mid
|
|
60
|
+
} catch {}
|
|
61
|
+
}
|
|
62
|
+
return os.hostname()
|
|
37
63
|
}
|
|
38
64
|
|
|
39
65
|
function getArch() {
|
|
66
|
+
const plat = process.platform === 'win32' ? 'windows' : process.platform === 'linux' ? 'linux' : 'darwin'
|
|
40
67
|
const arch = process.arch
|
|
41
|
-
if (arch === 'arm64') return
|
|
42
|
-
if (arch === 'x64') return
|
|
43
|
-
throw new Error(`
|
|
68
|
+
if (arch === 'arm64') return `${plat}-arm64`
|
|
69
|
+
if (arch === 'x64') return `${plat}-amd64`
|
|
70
|
+
throw new Error(`Unsupported architecture: ${plat}/${arch}. LMCP requires arm64 or x64.`)
|
|
44
71
|
}
|
|
45
72
|
|
|
46
73
|
/**
|
|
@@ -121,7 +148,55 @@ async function ensureBinary() {
|
|
|
121
148
|
const arch = getArch()
|
|
122
149
|
const info = await getLatestBinary()
|
|
123
150
|
const version = info.version
|
|
124
|
-
|
|
151
|
+
|
|
152
|
+
// On non-darwin, use the Go server binary (cross-platform).
|
|
153
|
+
// The Go binary is a single file, not a nested tarball.
|
|
154
|
+
if (process.platform !== 'darwin') {
|
|
155
|
+
const binName = process.platform === 'win32' ? 'lmcp-server.exe' : 'lmcp-server'
|
|
156
|
+
const binPath = path.join(CACHE_DIR, binName)
|
|
157
|
+
const versionFile = path.join(CACHE_DIR, '.go-server-version')
|
|
158
|
+
|
|
159
|
+
// Check if already cached at the right version
|
|
160
|
+
if (fs.existsSync(binPath)) {
|
|
161
|
+
try {
|
|
162
|
+
const cached = fs.readFileSync(versionFile, 'utf8').trim()
|
|
163
|
+
if (cached === version) return { binPath, versionDir: CACHE_DIR, version }
|
|
164
|
+
} catch {}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Download the Go binary tarball from R2
|
|
168
|
+
const url = `https://download.local-mcp.com/lmcp-server-${version}-${arch}.tar.gz`
|
|
169
|
+
process.stderr.write(`\nLMCP v${version} (${arch}) not found in cache.\n`)
|
|
170
|
+
process.stderr.write(`Downloading from ${url}\n`)
|
|
171
|
+
|
|
172
|
+
fs.mkdirSync(CACHE_DIR, { recursive: true })
|
|
173
|
+
const tarPath = path.join(CACHE_DIR, `go-server-${version}.tar.gz`)
|
|
174
|
+
|
|
175
|
+
try {
|
|
176
|
+
await downloadFile(url, tarPath)
|
|
177
|
+
process.stderr.write(` Extracting...\n`)
|
|
178
|
+
execFileSync('tar', ['-xzf', tarPath, '-C', CACHE_DIR], { stdio: 'pipe' })
|
|
179
|
+
fs.unlinkSync(tarPath)
|
|
180
|
+
} catch (err) {
|
|
181
|
+
// Go binary not yet published for this version — fall back to message
|
|
182
|
+
try { fs.unlinkSync(tarPath) } catch {}
|
|
183
|
+
throw new Error(
|
|
184
|
+
`LMCP ${version} is not yet available for ${arch}.\n` +
|
|
185
|
+
`The Windows/Linux Go server is in preview — check https://local-mcp.com for updates.`
|
|
186
|
+
)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (!fs.existsSync(binPath)) {
|
|
190
|
+
throw new Error(`Binary not found after extraction: ${binPath}`)
|
|
191
|
+
}
|
|
192
|
+
if (process.platform !== 'win32') fs.chmodSync(binPath, 0o755)
|
|
193
|
+
fs.writeFileSync(versionFile, version)
|
|
194
|
+
|
|
195
|
+
process.stderr.write(` Ready at ${CACHE_DIR}\n`)
|
|
196
|
+
return { binPath, versionDir: CACHE_DIR, version }
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// macOS: Swift binary in a nested tarball
|
|
125
200
|
const url = `https://download.local-mcp.com/local-mcp-server-${version}-${arch}.tar.gz`
|
|
126
201
|
|
|
127
202
|
const versionDir = path.join(CACHE_DIR, version)
|
|
@@ -148,28 +223,19 @@ async function ensureBinary() {
|
|
|
148
223
|
throw new Error(`Binario no encontrado después de extraer: ${binPath}`)
|
|
149
224
|
}
|
|
150
225
|
|
|
151
|
-
// Asegurar que sea ejecutable
|
|
152
226
|
fs.chmodSync(binPath, 0o755)
|
|
153
227
|
|
|
154
|
-
|
|
155
|
-
// on some macOS versions can inherit it from the archive. Non-critical.
|
|
156
|
-
try { execFileSync('xattr', ['-d', 'com.apple.quarantine', binPath], { stdio: 'pipe' }) } catch { /* no quarantine, fine */ }
|
|
228
|
+
try { execFileSync('xattr', ['-d', 'com.apple.quarantine', binPath], { stdio: 'pipe' }) } catch {}
|
|
157
229
|
|
|
158
|
-
// Preserve the Developer ID signature from the build pipeline.
|
|
159
|
-
// On macOS 26+ ad-hoc signatures (--sign -) don't create a stable TCC identity:
|
|
160
|
-
// each new CDHash is treated as a new app, triggering "node would like to access
|
|
161
|
-
// data from other apps" on every JXA call. Only fall back to ad-hoc if the binary
|
|
162
|
-
// has no valid signature at all (e.g. a dev build that was never properly signed).
|
|
163
|
-
// See: FB-74E468, FB-017B8E — permission prompt loop on macOS 26 beta.
|
|
164
230
|
let hasSig = false
|
|
165
231
|
try {
|
|
166
232
|
execFileSync('codesign', ['--verify', binPath], { stdio: 'pipe' })
|
|
167
233
|
hasSig = true
|
|
168
|
-
} catch {
|
|
234
|
+
} catch {}
|
|
169
235
|
if (!hasSig) {
|
|
170
236
|
try {
|
|
171
237
|
execFileSync('codesign', ['--force', '--sign', '-', '--identifier', 'com.local-mcp.server', binPath], { stdio: 'pipe' })
|
|
172
|
-
} catch {
|
|
238
|
+
} catch {}
|
|
173
239
|
}
|
|
174
240
|
|
|
175
241
|
process.stderr.write(` Listo en ${versionDir}\n`)
|
package/index.js
CHANGED
|
@@ -18,9 +18,24 @@ const fs = require('fs')
|
|
|
18
18
|
const https = require('https')
|
|
19
19
|
|
|
20
20
|
|
|
21
|
-
//
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
// Platform support: macOS (full), Windows (Go server), Linux (Go server)
|
|
22
|
+
const SUPPORTED_PLATFORMS = ['darwin', 'win32', 'linux']
|
|
23
|
+
if (!SUPPORTED_PLATFORMS.includes(process.platform)) {
|
|
24
|
+
console.error(`LMCP is not available for ${process.platform}. Supported: macOS, Windows, Linux.`)
|
|
25
|
+
process.exit(1)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Node version guard — Claude Desktop can silently launch LMCP with an old
|
|
29
|
+
// nvm default (e.g. v11) that makes `npx -y` fail before this file loads.
|
|
30
|
+
// If this code IS running on an old Node, surface the error clearly so the
|
|
31
|
+
// user sees it in Claude Desktop's MCP logs instead of a cryptic spawn error.
|
|
32
|
+
if (parseInt(process.version.slice(1)) < 16) {
|
|
33
|
+
const msg = `LMCP requires Node 16+. Running ${process.version}.\n` +
|
|
34
|
+
`If you use nvm, run: nvm alias default 22 && nvm uninstall ${process.version}\n` +
|
|
35
|
+
'Then restart Claude Desktop / Cursor.'
|
|
36
|
+
process.stderr.write(msg + '\n')
|
|
37
|
+
// Exit with a non-zero code so the MCP host marks the server as failed
|
|
38
|
+
// (vs hanging forever with no output).
|
|
24
39
|
process.exit(1)
|
|
25
40
|
}
|
|
26
41
|
|
|
@@ -75,7 +90,8 @@ async function main() {
|
|
|
75
90
|
|
|
76
91
|
// ── Modo default: stdio MCP server ──────────────────────────────────────────
|
|
77
92
|
const { CACHE_DIR } = require('./download')
|
|
78
|
-
const
|
|
93
|
+
const binName = process.platform === 'win32' ? 'local-mcp-server.exe' : 'local-mcp-server'
|
|
94
|
+
const stableBin = path.join(CACHE_DIR, binName)
|
|
79
95
|
const versionFile = path.join(CACHE_DIR, '.server-version')
|
|
80
96
|
const pkg = require('./package.json')
|
|
81
97
|
|
|
@@ -103,11 +119,13 @@ async function main() {
|
|
|
103
119
|
: ''
|
|
104
120
|
if (cachedVersion && semverGte(cachedVersion, pkg.version)) {
|
|
105
121
|
// Cached binary is same or newer — use fast path, no download needed
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
122
|
+
if (process.platform === 'darwin') {
|
|
123
|
+
const { ensureTray, ensureTeamsProxy, ensureHelper, ensureJXARunner } = require('./download')
|
|
124
|
+
ensureTray().catch(() => {})
|
|
125
|
+
ensureTeamsProxy().catch(() => {})
|
|
126
|
+
ensureHelper().catch(() => {})
|
|
127
|
+
ensureJXARunner().catch(() => {})
|
|
128
|
+
}
|
|
111
129
|
const child = spawn(stableBin, [], { stdio: 'inherit', env: process.env })
|
|
112
130
|
child.on('error', () => process.exit(1))
|
|
113
131
|
child.on('exit', (code) => process.exit(code ?? 0))
|
|
@@ -136,7 +154,7 @@ async function main() {
|
|
|
136
154
|
const tmpPath = stableBin + '.tmp'
|
|
137
155
|
fs.copyFileSync(binPath, tmpPath)
|
|
138
156
|
fs.chmodSync(tmpPath, 0o755)
|
|
139
|
-
try { execFileSync('codesign', ['--force', '--sign', '-', '--identifier', 'com.local-mcp.server', tmpPath], { stdio: 'pipe' }) } catch {}
|
|
157
|
+
if (process.platform === 'darwin') { try { execFileSync('codesign', ['--force', '--sign', '-', '--identifier', 'com.local-mcp.server', tmpPath], { stdio: 'pipe' }) } catch {} }
|
|
140
158
|
fs.renameSync(tmpPath, stableBin)
|
|
141
159
|
fs.writeFileSync(versionFile, downloadedVersion || latestVersion)
|
|
142
160
|
} catch {}
|
|
@@ -165,11 +183,13 @@ async function main() {
|
|
|
165
183
|
// Slow path: download/repair binary
|
|
166
184
|
const { ensureRuntime, ensureTray, ensureTeamsProxy, ensureHelper, ensureJXARunner } = require('./download')
|
|
167
185
|
|
|
168
|
-
// Update tray + proxies in background
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
186
|
+
// Update tray + proxies in background (macOS only).
|
|
187
|
+
if (process.platform === 'darwin') {
|
|
188
|
+
ensureTray().catch(() => {})
|
|
189
|
+
ensureTeamsProxy().catch(() => {})
|
|
190
|
+
ensureHelper().catch(() => {})
|
|
191
|
+
ensureJXARunner().catch(() => {})
|
|
192
|
+
}
|
|
173
193
|
|
|
174
194
|
let runtime
|
|
175
195
|
try {
|
|
@@ -187,7 +207,7 @@ async function main() {
|
|
|
187
207
|
const tmpPath = stableBin + '.tmp'
|
|
188
208
|
fs.copyFileSync(binPath, tmpPath)
|
|
189
209
|
fs.chmodSync(tmpPath, 0o755)
|
|
190
|
-
try { execFileSync('codesign', ['--force', '--sign', '-', '--identifier', 'com.local-mcp.server', tmpPath], { stdio: 'pipe' }) } catch {}
|
|
210
|
+
if (process.platform === 'darwin') { try { execFileSync('codesign', ['--force', '--sign', '-', '--identifier', 'com.local-mcp.server', tmpPath], { stdio: 'pipe' }) } catch {} }
|
|
191
211
|
fs.renameSync(tmpPath, stableBin)
|
|
192
212
|
// Write the ACTUAL downloaded binary version (not pkg.version) so the fast path
|
|
193
213
|
// knows what version is on disk. Using pkg.version here caused LMC-375: machines
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "local-mcp",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.139",
|
|
4
4
|
"description": "LMCP — connect Claude Desktop, Cursor, Windsurf to Mail, Calendar, Contacts, Teams, OneDrive on macOS. Privacy-first: all data stays on your Mac.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -20,7 +20,9 @@
|
|
|
20
20
|
"node": ">=18"
|
|
21
21
|
},
|
|
22
22
|
"os": [
|
|
23
|
-
"darwin"
|
|
23
|
+
"darwin",
|
|
24
|
+
"win32",
|
|
25
|
+
"linux"
|
|
24
26
|
],
|
|
25
27
|
"keywords": [
|
|
26
28
|
"mcp",
|
package/setup.js
CHANGED
|
@@ -16,17 +16,83 @@ const { execSync, execFileSync } = require('child_process')
|
|
|
16
16
|
const HOME = os.homedir()
|
|
17
17
|
const NPX_COMMAND = 'npx'
|
|
18
18
|
const NPX_ARGS = ['-y', 'local-mcp@latest']
|
|
19
|
+
|
|
20
|
+
// Resolve absolute path to npx from the current Node binary so Claude Desktop
|
|
21
|
+
// doesn't pick up an old npx from its own PATH (common with nvm users whose
|
|
22
|
+
// nvm default points to Node v11/v12 — those npx versions don't support -y).
|
|
23
|
+
// Falls back to plain 'npx' if resolution fails (non-nvm setups, symlinks, etc.)
|
|
24
|
+
function _resolveNpxPath() {
|
|
25
|
+
try {
|
|
26
|
+
const candidate = path.join(path.dirname(process.execPath), 'npx')
|
|
27
|
+
if (fs.existsSync(candidate)) return candidate
|
|
28
|
+
} catch {}
|
|
29
|
+
return NPX_COMMAND
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Write a shell launcher script that wraps npx with a Node version check and
|
|
33
|
+
// curl-based telemetry. This gives us visibility into failures that happen
|
|
34
|
+
// BEFORE Node loads our code (e.g. old nvm default in Claude Desktop's PATH).
|
|
35
|
+
// Returns the path to the launcher, or null if writing fails.
|
|
36
|
+
function _writeLaunchScript(npxAbsPath, cacheDir) {
|
|
37
|
+
try {
|
|
38
|
+
fs.mkdirSync(cacheDir, { recursive: true })
|
|
39
|
+
const launcherPath = path.join(cacheDir, 'lmcp-launch.sh')
|
|
40
|
+
const script = [
|
|
41
|
+
'#!/bin/bash',
|
|
42
|
+
'# LMCP launcher — generated by npx local-mcp setup, do not edit.',
|
|
43
|
+
'# Checks Node version before launching npx; sends telemetry if too old.',
|
|
44
|
+
'NODE_VER=$(node --version 2>/dev/null || echo "none")',
|
|
45
|
+
'NODE_MAJOR=$(echo "$NODE_VER" | sed \'s/v//\' | cut -d. -f1)',
|
|
46
|
+
'if [ "${NODE_MAJOR:-0}" -lt 16 ] 2>/dev/null; then',
|
|
47
|
+
' # curl telemetry — no Node dependency, fire-and-forget',
|
|
48
|
+
` curl -sf --max-time 3 -X POST "https://${BACKEND_HOST}/install-event" \\`,
|
|
49
|
+
' -H "Content-Type: application/json" \\',
|
|
50
|
+
' -d "{\\\"stage\\\":\\\"node_too_old\\\",\\\"version\\\":\\\"$NODE_VER\\\"}" &>/dev/null &',
|
|
51
|
+
' printf \'LMCP requires Node 16+. Current: %s\\nFix: nvm alias default 22 && nvm uninstall %s — then restart Claude Desktop.\\n\' "$NODE_VER" "$NODE_VER" >&2',
|
|
52
|
+
' exit 1',
|
|
53
|
+
'fi',
|
|
54
|
+
`exec "${npxAbsPath}" -y local-mcp@latest "$@"`,
|
|
55
|
+
].join('\n') + '\n'
|
|
56
|
+
fs.writeFileSync(launcherPath, script, { mode: 0o755 })
|
|
57
|
+
return launcherPath
|
|
58
|
+
} catch {
|
|
59
|
+
return null
|
|
60
|
+
}
|
|
61
|
+
}
|
|
19
62
|
const STABLE_LINK = path.join(os.homedir(), '.local', 'share', 'local-mcp', 'bin', 'local-mcp-server')
|
|
20
63
|
const BACKEND_HOST = 'office-mcp-production.up.railway.app'
|
|
21
64
|
|
|
22
|
-
// ──
|
|
65
|
+
// ── Platform-aware config paths for AI clients ─────────────────────────────
|
|
66
|
+
// macOS: ~/Library/Application Support/...
|
|
67
|
+
// Windows: %APPDATA%/... (most Electron apps use APPDATA)
|
|
68
|
+
// Linux: ~/.config/... (XDG standard)
|
|
69
|
+
|
|
70
|
+
const _IS_WIN = process.platform === 'win32'
|
|
71
|
+
const _IS_MAC = process.platform === 'darwin'
|
|
72
|
+
const _APPDATA = process.env.APPDATA || path.join(HOME, 'AppData', 'Roaming')
|
|
73
|
+
|
|
74
|
+
function _claudeDesktopPath() {
|
|
75
|
+
if (_IS_WIN) return path.join(_APPDATA, 'Claude', 'claude_desktop_config.json')
|
|
76
|
+
if (_IS_MAC) return path.join(HOME, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json')
|
|
77
|
+
return path.join(HOME, '.config', 'Claude', 'claude_desktop_config.json')
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function _claudeDesktopDetect() {
|
|
81
|
+
return fs.existsSync(path.dirname(_claudeDesktopPath()))
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function _rooClinePath() {
|
|
85
|
+
if (_IS_WIN) return path.join(_APPDATA, 'Code', 'User', 'globalStorage', 'rooveterinaryinc.roo-cline', 'mcp_settings.json')
|
|
86
|
+
if (_IS_MAC) return path.join(HOME, 'Library', 'Application Support', 'Code', 'User', 'globalStorage', 'rooveterinaryinc.roo-cline', 'mcp_settings.json')
|
|
87
|
+
return path.join(HOME, '.config', 'Code', 'User', 'globalStorage', 'rooveterinaryinc.roo-cline', 'mcp_settings.json')
|
|
88
|
+
}
|
|
23
89
|
|
|
24
90
|
const CLIENTS = [
|
|
25
91
|
{
|
|
26
92
|
id: 'claude-desktop',
|
|
27
93
|
name: 'Claude Desktop',
|
|
28
|
-
cfgPath:
|
|
29
|
-
detect:
|
|
94
|
+
cfgPath: _claudeDesktopPath(),
|
|
95
|
+
detect: _claudeDesktopDetect,
|
|
30
96
|
},
|
|
31
97
|
{
|
|
32
98
|
id: 'cursor',
|
|
@@ -50,8 +116,8 @@ const CLIENTS = [
|
|
|
50
116
|
{
|
|
51
117
|
id: 'roo-cline',
|
|
52
118
|
name: 'Roo-Cline',
|
|
53
|
-
cfgPath:
|
|
54
|
-
detect: () => fs.existsSync(path.
|
|
119
|
+
cfgPath: _rooClinePath(),
|
|
120
|
+
detect: () => fs.existsSync(path.dirname(_rooClinePath())),
|
|
55
121
|
},
|
|
56
122
|
{
|
|
57
123
|
id: 'zed',
|
|
@@ -71,12 +137,25 @@ const CLIENTS = [
|
|
|
71
137
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
72
138
|
|
|
73
139
|
function _appExists(name) {
|
|
74
|
-
|
|
75
|
-
|
|
140
|
+
if (_IS_MAC) {
|
|
141
|
+
return fs.existsSync(`/Applications/${name}.app`) ||
|
|
142
|
+
fs.existsSync(path.join(HOME, `Applications/${name}.app`))
|
|
143
|
+
}
|
|
144
|
+
if (_IS_WIN) {
|
|
145
|
+
// Check common Windows install locations
|
|
146
|
+
const pf = process.env.ProgramFiles || 'C:\\Program Files'
|
|
147
|
+
const pf86 = process.env['ProgramFiles(x86)'] || 'C:\\Program Files (x86)'
|
|
148
|
+
const localApp = process.env.LOCALAPPDATA || path.join(HOME, 'AppData', 'Local')
|
|
149
|
+
return fs.existsSync(path.join(pf, name)) ||
|
|
150
|
+
fs.existsSync(path.join(pf86, name)) ||
|
|
151
|
+
fs.existsSync(path.join(localApp, name))
|
|
152
|
+
}
|
|
153
|
+
return false // Linux: rely on _cmdExists or config path detection
|
|
76
154
|
}
|
|
77
155
|
|
|
78
156
|
function _cmdExists(cmd) {
|
|
79
|
-
|
|
157
|
+
const which = _IS_WIN ? 'where' : 'which'
|
|
158
|
+
try { execSync(`${which} ${cmd}`, { stdio: 'pipe' }); return true } catch { return false }
|
|
80
159
|
}
|
|
81
160
|
|
|
82
161
|
// ── Safe config read ─────────────────────────────────────────────────────────
|
|
@@ -212,9 +291,21 @@ async function runSetup(opts = {}) {
|
|
|
212
291
|
console.log('║ LMCP — Setup Wizard ║')
|
|
213
292
|
console.log('╚══════════════════════════════════════╝\n')
|
|
214
293
|
|
|
294
|
+
// Warn if running on an old Node version. Claude Desktop resolves 'npx' from
|
|
295
|
+
// its own PATH (not the user's shell), so an old nvm default (v11/v12) causes
|
|
296
|
+
// a silent "You must supply a command" failure on first launch.
|
|
297
|
+
const nodeMajor = parseInt(process.version.slice(1))
|
|
298
|
+
if (nodeMajor < 16) {
|
|
299
|
+
console.error(`\n⚠️ Warning: you're running Node ${process.version}.`)
|
|
300
|
+
console.error(' Claude Desktop may fail to start LMCP because it finds an old npx on its PATH.')
|
|
301
|
+
console.error(` Fix: nvm alias default 22 && nvm uninstall ${process.version} (then relaunch Claude Desktop)\n`)
|
|
302
|
+
}
|
|
303
|
+
|
|
215
304
|
// Pre-download binary so first launch is instant
|
|
216
|
-
// All clients use
|
|
217
|
-
|
|
305
|
+
// All clients use a shell launcher that wraps npx (self-healing if binary is
|
|
306
|
+
// missing/broken, and surfaces telemetry + clear errors if Node is too old).
|
|
307
|
+
let npxAbsPath = _resolveNpxPath() // baked into the launcher script
|
|
308
|
+
let stableCommand = NPX_COMMAND // fallback; replaced below once CACHE_DIR is known
|
|
218
309
|
let stableArgs = NPX_ARGS
|
|
219
310
|
let binaryVersion = ''
|
|
220
311
|
try {
|
|
@@ -252,11 +343,25 @@ async function runSetup(opts = {}) {
|
|
|
252
343
|
if (fs.existsSync(settingsSrc)) fs.copyFileSync(settingsSrc, settingsDst)
|
|
253
344
|
} catch {}
|
|
254
345
|
process.stderr.write('✓ Runtime ready\n\n')
|
|
346
|
+
// Write the launcher script now that we have CACHE_DIR.
|
|
347
|
+
// The launcher uses curl (no Node dependency) to send telemetry if Node is
|
|
348
|
+
// too old, then exec's npx. Claude Desktop runs this script instead of npx
|
|
349
|
+
// directly, so failures are visible in MCP logs AND in our backend events.
|
|
350
|
+
const launcherPath = _writeLaunchScript(npxAbsPath, CACHE_DIR)
|
|
351
|
+
if (launcherPath) stableCommand = launcherPath
|
|
255
352
|
} catch (err) {
|
|
256
353
|
process.stderr.write(` (Runtime download failed, will download on first run: ${err.message})\n\n`)
|
|
354
|
+
// Still try to write a launcher even if binary download failed — the version
|
|
355
|
+
// check + telemetry path is independent of the binary.
|
|
356
|
+
try {
|
|
357
|
+
const { CACHE_DIR } = require('./download')
|
|
358
|
+
const launcherPath = _writeLaunchScript(npxAbsPath, CACHE_DIR)
|
|
359
|
+
if (launcherPath) stableCommand = launcherPath
|
|
360
|
+
} catch {}
|
|
257
361
|
}
|
|
258
|
-
//
|
|
259
|
-
//
|
|
362
|
+
// stableCommand is now the shell launcher script path. It wraps npx with a
|
|
363
|
+
// Node version check + curl telemetry before exec'ing. If launcher creation
|
|
364
|
+
// failed it falls back to plain 'npx'.
|
|
260
365
|
|
|
261
366
|
// Detectar clientes
|
|
262
367
|
const detected = CLIENTS.filter(c => c.detect())
|