local-mcp 3.0.378 → 3.0.380
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 +102 -35
- package/package.json +1 -1
package/download.js
CHANGED
|
@@ -52,6 +52,10 @@ const TRAY_APP = process.platform === 'darwin'
|
|
|
52
52
|
? path.join(os.homedir(), 'Applications', 'LocalMCPTray.app')
|
|
53
53
|
: null
|
|
54
54
|
|
|
55
|
+
// La instalación vieja que TRAY_APP supersede. Const (no literal suelto) porque el orden en
|
|
56
|
+
// que se la retira es lo que arregla #2654 y los tests la inyectan.
|
|
57
|
+
const SYSTEM_TRAY_APP = '/Applications/LocalMCPTray.app'
|
|
58
|
+
|
|
55
59
|
// Mirror of go-server's CacheDir()/.machine-id (internal/machineid/machineid.go).
|
|
56
60
|
// Only the win32 ladder reads/writes it: it's the side of the probe that can
|
|
57
61
|
// come up empty on a real machine (wmic gone, PowerShell blocked by policy),
|
|
@@ -532,12 +536,65 @@ async function ensureTeamsProxy(platform = process.platform) {
|
|
|
532
536
|
* Descarga e instala el tray SwiftUI (menu bar app) — universal binary (arm64 + Intel).
|
|
533
537
|
* @returns {Promise<string|null>} Ruta al .app instalado, o null si falla
|
|
534
538
|
*/
|
|
535
|
-
|
|
539
|
+
// Un .app "usable" es el EJECUTABLE adentro, no el directorio: una extracción a medias deja
|
|
540
|
+
// el bundle existiendo y vacío, y esa fue la falla de #1338. (#2654)
|
|
541
|
+
function _isUsableTrayBundle(trayApp) {
|
|
542
|
+
try { return fs.existsSync(path.join(trayApp, 'Contents', 'MacOS', 'LocalMCPTray')) }
|
|
543
|
+
catch { return false }
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// Retira las copias que `trayApp` supersede — SOLO si `trayApp` ya tiene un bundle usable.
|
|
547
|
+
// Sin esa guarda, la migración borra la única copia que el Mac tiene antes de que exista su
|
|
548
|
+
// reemplazo, y una descarga fallida deja la máquina sin cliente (#2654, hermano de #2643).
|
|
549
|
+
// Devuelve lo retirado para que los tests puedan afirmar sobre el efecto, no sobre el orden.
|
|
550
|
+
function _retireSupersededTrays(trayApp, supersededPaths, rm = _rmrf) {
|
|
551
|
+
if (!trayApp || !_isUsableTrayBundle(trayApp)) return []
|
|
552
|
+
const retired = []
|
|
553
|
+
for (const p of supersededPaths) {
|
|
554
|
+
if (!p) continue
|
|
555
|
+
if (_sameLocation(p, trayApp)) continue
|
|
556
|
+
if (!fs.existsSync(p)) continue
|
|
557
|
+
try { rm(p); retired.push(p) } catch { /* ignorar */ }
|
|
558
|
+
}
|
|
559
|
+
return retired
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// Identidad, no grafía: con ~/Applications symlinkeado a /Applications los dos nombres son el
|
|
563
|
+
// MISMO directorio, y un path.resolve() los ve distintos — el retiro se llevaba puesto el bundle
|
|
564
|
+
// recién instalado y la máquina no volvía a tener cliente nunca. Cubre symlinks, no diferencias
|
|
565
|
+
// de mayúsculas: realpathSync no normaliza case en APFS, y ningún alias así es alcanzable desde
|
|
566
|
+
// TRAY_APP/SYSTEM_TRAY_APP, que son constantes. (#2654)
|
|
567
|
+
function _sameLocation(a, b) {
|
|
568
|
+
try { return fs.realpathSync(a) === fs.realpathSync(b) }
|
|
569
|
+
catch { return path.resolve(a) === path.resolve(b) }
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function _rmrf(p) {
|
|
573
|
+
execFileSync('rm', ['-rf', p], { stdio: 'pipe' })
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// `deps` es la costura que hace testeable el ORDEN de esta función sin red ni HOME real:
|
|
577
|
+
// los tests de #2654 la corren entera con dirs temporales y una descarga que falla. Mismo
|
|
578
|
+
// idioma que _getMachineId(deps = {}). Producción no pasa nada y usa los valores de módulo.
|
|
579
|
+
async function ensureTray(deps = {}) {
|
|
580
|
+
const {
|
|
581
|
+
platform = process.platform,
|
|
582
|
+
trayApp = TRAY_APP,
|
|
583
|
+
trayDir = TRAY_DIR,
|
|
584
|
+
systemTrayApp = SYSTEM_TRAY_APP,
|
|
585
|
+
uninstalledMarkers = null,
|
|
586
|
+
getLatest = getLatestBinary,
|
|
587
|
+
download = downloadFile,
|
|
588
|
+
extract = extractTar,
|
|
589
|
+
writeLaunchAgent = _writeTrayLaunchAgent,
|
|
590
|
+
isTTY = () => process.stdin.isTTY,
|
|
591
|
+
} = deps
|
|
592
|
+
|
|
536
593
|
// Windows/Linux: the tray + background daemon are installed by the native
|
|
537
594
|
// installer (LMCP-Setup.exe on Windows; the systemd unit on Linux), NOT by npm.
|
|
538
595
|
// The npm package is a stdio proxy for MCP hosts, so there is no tray to fetch
|
|
539
596
|
// here — no-op instead of downloading a lmcp-tray binary.
|
|
540
|
-
if (
|
|
597
|
+
if (platform !== 'darwin') {
|
|
541
598
|
return null
|
|
542
599
|
}
|
|
543
600
|
|
|
@@ -545,47 +602,51 @@ async function ensureTray() {
|
|
|
545
602
|
// Checks BOTH the runtime-dir copy and the durable App Support copy: a user who
|
|
546
603
|
// "cleaned up" with `rm -rf ~/.local/share/local-mcp` removes the runtime-dir copy,
|
|
547
604
|
// and only the durable one survives to stop a resurrection loop (FB-D1FA79).
|
|
548
|
-
const
|
|
605
|
+
const markers = uninstalledMarkers || [
|
|
549
606
|
path.join(CACHE_DIR, '..', '.uninstalled'),
|
|
550
607
|
path.join(os.homedir(), 'Library', 'Application Support', 'Local MCP', '.uninstalled'),
|
|
551
608
|
]
|
|
552
|
-
if (
|
|
609
|
+
if (markers.some((p) => { try { return fs.existsSync(p) } catch { return false } })) return null
|
|
553
610
|
|
|
554
|
-
const info = await
|
|
611
|
+
const info = await getLatest()
|
|
555
612
|
const version = info.version
|
|
556
|
-
const
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
//
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
if (fs.readFileSync(verFile, 'utf8').trim() === version) return trayApp
|
|
613
|
+
const verFile = path.join(trayDir, '.version')
|
|
614
|
+
|
|
615
|
+
// Las dos migraciones —~/.local/share (legacy) y /Applications → ~/Applications (evita el
|
|
616
|
+
// diálogo App Management)— retiran una copia que puede ser la ÚNICA del Mac. Por eso el
|
|
617
|
+
// retiro corre en exactamente dos lugares: pegado a cada early-return (no se instala nada,
|
|
618
|
+
// el tray de destino ya está verificado) y después de extraer y verificar. Nunca en el
|
|
619
|
+
// camino que descarga: ahí una falla dejaba la máquina sin cliente. (#2654)
|
|
620
|
+
const superseded = [path.join(trayDir, 'LocalMCPTray.app'), systemTrayApp]
|
|
621
|
+
const retireSuperseded = () => _retireSupersededTrays(trayApp, superseded)
|
|
622
|
+
|
|
623
|
+
// Ya instalado y actualizado. "Instalado" es el bundle USABLE, no el directorio: si no,
|
|
624
|
+
// una extracción a medias se early-returnea para siempre y nadie la repara (#1338).
|
|
625
|
+
if (_isUsableTrayBundle(trayApp) && fs.existsSync(verFile)) {
|
|
626
|
+
if (fs.readFileSync(verFile, 'utf8').trim() === version) {
|
|
627
|
+
retireSuperseded()
|
|
628
|
+
return trayApp
|
|
629
|
+
}
|
|
574
630
|
}
|
|
575
631
|
|
|
576
632
|
// In an active MCP session (non-TTY) the tray is running and self-updates via UpdateManager.
|
|
577
633
|
// Downloading a new tray bundle and rm-rf'ing the old one mid-session kills the Unix socket
|
|
578
634
|
// that local-mcp-server uses, crashing the session. Skip the update; it'll apply next login.
|
|
579
|
-
|
|
635
|
+
// Un bundle sin ejecutable no tiene tray corriendo que esta descarga pueda romper, así que
|
|
636
|
+
// ese caso NO se early-returnea: se repara.
|
|
637
|
+
if (!isTTY() && _isUsableTrayBundle(trayApp)) {
|
|
638
|
+
retireSuperseded()
|
|
639
|
+
return trayApp
|
|
640
|
+
}
|
|
580
641
|
|
|
581
642
|
const url = `https://download.local-mcp.com/local-mcp-tray-${version}-darwin-universal.tar.gz`
|
|
582
643
|
process.stderr.write(`\nDescargando tray v${version}...\n`)
|
|
583
644
|
|
|
584
|
-
fs.mkdirSync(
|
|
585
|
-
const tarPath = path.join(
|
|
645
|
+
fs.mkdirSync(trayDir, { recursive: true })
|
|
646
|
+
const tarPath = path.join(trayDir, `tray-${version}.tar.gz`)
|
|
586
647
|
|
|
587
648
|
// Descargar ANTES de borrar el viejo — si la descarga falla, el tray actual sigue intacto
|
|
588
|
-
await
|
|
649
|
+
await download(url, tarPath)
|
|
589
650
|
|
|
590
651
|
// Verificar que el archivo descargado no está vacío
|
|
591
652
|
const tarStat = fs.statSync(tarPath)
|
|
@@ -596,33 +657,38 @@ async function ensureTray() {
|
|
|
596
657
|
|
|
597
658
|
// Recién ahora borrar el viejo e instalar el nuevo
|
|
598
659
|
if (fs.existsSync(trayApp)) {
|
|
599
|
-
try {
|
|
660
|
+
try { _rmrf(trayApp) } catch { /* ignorar */ }
|
|
600
661
|
}
|
|
601
662
|
|
|
602
663
|
// Crear ~/Applications si no existe (ubicación estándar para apps de usuario)
|
|
603
|
-
const userAppsDir = path.
|
|
664
|
+
const userAppsDir = path.dirname(trayApp)
|
|
604
665
|
fs.mkdirSync(userAppsDir, { recursive: true })
|
|
605
666
|
|
|
606
667
|
process.stderr.write(` Instalando tray en ~/Applications...\n`)
|
|
607
|
-
|
|
668
|
+
extract(tarPath, userAppsDir)
|
|
608
669
|
fs.unlinkSync(tarPath)
|
|
609
670
|
|
|
610
|
-
|
|
611
|
-
|
|
671
|
+
// Mismo criterio que usa el retiro, o se estampa .version sobre un bundle roto y el
|
|
672
|
+
// early-return de arriba lo da por bueno en cada corrida siguiente.
|
|
673
|
+
if (!_isUsableTrayBundle(trayApp)) {
|
|
674
|
+
throw new Error(`Tray no usable tras extraer: ${trayApp}`)
|
|
612
675
|
}
|
|
613
676
|
|
|
677
|
+
// El reemplazo está instalado y verificado: recién ACÁ se retira lo que supersede.
|
|
678
|
+
retireSuperseded()
|
|
679
|
+
|
|
614
680
|
// Quitar quarantine — la app viene firmada con Developer ID desde CI, no re-firmar
|
|
615
681
|
try { execFileSync('xattr', ['-rd', 'com.apple.quarantine', trayApp], { stdio: 'pipe' }) } catch { /* no crítico */ }
|
|
616
682
|
|
|
617
683
|
fs.writeFileSync(verFile, version, 'utf8')
|
|
618
684
|
// Keep in sync with UpdateManager / Heartbeat (was .version only → fleet stuck on old tray)
|
|
619
|
-
const trayVerFile = path.join(
|
|
685
|
+
const trayVerFile = path.join(trayDir, '.tray-version')
|
|
620
686
|
fs.writeFileSync(trayVerFile, version, 'utf8')
|
|
621
687
|
|
|
622
688
|
// LaunchAgent — auto-start en cada reboot de sesión
|
|
623
|
-
|
|
689
|
+
writeLaunchAgent(trayApp)
|
|
624
690
|
|
|
625
|
-
process.stderr.write(` Tray instalado en ${trayApp}${
|
|
691
|
+
process.stderr.write(` Tray instalado en ${trayApp}${isTTY() ? '' : ' (se iniciará en el próximo login)'}\n`)
|
|
626
692
|
return trayApp
|
|
627
693
|
}
|
|
628
694
|
|
|
@@ -727,4 +793,5 @@ module.exports = {
|
|
|
727
793
|
CACHE_DIR, TRAY_DIR, _getMachineId,
|
|
728
794
|
_probeWinHardwareId, _resetWinHardwareIdCacheForTests,
|
|
729
795
|
_readWinMachineIdCache, _writeWinMachineIdCache, WIN_MACHINE_ID_CACHE_NAME,
|
|
796
|
+
_retireSupersededTrays, _isUsableTrayBundle, SYSTEM_TRAY_APP,
|
|
730
797
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "local-mcp",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.380",
|
|
4
4
|
"description": "Let ChatGPT, Claude, Cursor & any MCP client actually use your Mac — read & reply to email, manage your calendar, text over iMessage, find files, work with Teams, Slack & Office. On your Mac, no API keys, free.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|