local-mcp 3.0.416 → 3.0.418
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/README.md +2 -2
- package/download.js +70 -21
- package/index.js +51 -13
- package/launch-event.js +194 -0
- package/package.json +4 -3
- package/setup.js +35 -23
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
[](https://local-mcp.com?ref=npm)
|
|
7
7
|
[](https://local-mcp.com?ref=npm)
|
|
8
8
|
[](https://local-mcp.com/en/privacy)
|
|
9
|
-
[](https://glama.ai/mcp/servers/colibird-ai/local-mcp-releases?ref=npm)
|
|
10
10
|
|
|
11
11
|
**Runs on your computer. No tokens. No API keys. Free.**
|
|
12
12
|
|
|
@@ -203,4 +203,4 @@ Stops all background processes, removes the LaunchAgent, deletes the app and bin
|
|
|
203
203
|
|
|
204
204
|
## Links
|
|
205
205
|
|
|
206
|
-
[Website](https://local-mcp.com?ref=npm) · [GitHub](https://github.com/
|
|
206
|
+
[Website](https://local-mcp.com?ref=npm) · [GitHub](https://github.com/colibird-ai/local-mcp-releases) · [Guides](https://local-mcp.com/guides) · Support: ctpo@colibird.co
|
package/download.js
CHANGED
|
@@ -31,6 +31,7 @@ function extractTar(tarPath, destDir) {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
const BACKEND_URL = 'https://office-mcp-production.up.railway.app'
|
|
34
|
+
const LINUX_NO_SOPORTADO = 'Local MCP does not support Linux. It runs on macOS and Windows — https://local-mcp.com'
|
|
34
35
|
|
|
35
36
|
// Platform-aware cache directory:
|
|
36
37
|
// macOS: ~/.local/share/local-mcp/bin
|
|
@@ -108,6 +109,26 @@ function _writeWinMachineIdCache(cacheDir, id) {
|
|
|
108
109
|
// memoized too, on purpose: a run where PowerShell is blocked shouldn't retry
|
|
109
110
|
// it on every subsequent call in the same process.
|
|
110
111
|
let _winHardwareIdCache
|
|
112
|
+
// Valores que Win32_ComputerSystemProduct.UUID devuelve y que NO identifican a una máquina:
|
|
113
|
+
// relleno, o —el caso peligroso— el MISMO valor en máquinas distintas (defaults de OEM/BIOS).
|
|
114
|
+
// Aceptar uno junta a N máquinas en una identidad, y la separa de su propio heartbeat, porque el
|
|
115
|
+
// cliente Go lo rechaza. Copia a mano de `knownGarbageHardwareIDs` de lmcp-win
|
|
116
|
+
// (internal/machineid/probe_windows.go, #1594): si esa lista cambia, ésta cambia con ella
|
|
117
|
+
// (colibird-ai/lmcp-npm#29). En mayúsculas; la comparación no distingue mayúsculas.
|
|
118
|
+
const UUIDS_DE_HARDWARE_BASURA = new Set([
|
|
119
|
+
'NIL', // lo que imprime PowerShell cuando WMI no tiene nada que dar
|
|
120
|
+
'00000000-0000-0000-0000-000000000000',
|
|
121
|
+
'FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF',
|
|
122
|
+
'03000200-0400-0500-0006-000700080009', // el default clásico de OEM/BIOS
|
|
123
|
+
])
|
|
124
|
+
// Tope de cada llamada del probe (#42, O1). execSync bloquea el event loop: con WMI colgado, el
|
|
125
|
+
// proceso suelto del evento de lanzamiento no llegaba nunca a su propio tope de 4 s y quedaba vivo,
|
|
126
|
+
// oculto, para siempre. Con el tope, una llamada colgada se mata y el probe sigue (o termina vacío).
|
|
127
|
+
const TOPE_PROBE_MS = 2000
|
|
128
|
+
function _esIdDeHardwareValido(id) {
|
|
129
|
+
return !!id && !UUIDS_DE_HARDWARE_BASURA.has(String(id).toUpperCase())
|
|
130
|
+
}
|
|
131
|
+
|
|
111
132
|
// execImpl is injectable (defaults to the real execSync) so tests can pin the
|
|
112
133
|
// PowerShell-then-wmic ORDER without a Windows machine — see
|
|
113
134
|
// npm/test/machine-id-windows-order.test.js.
|
|
@@ -119,21 +140,24 @@ function _probeWinHardwareId(execImpl = require('child_process').execSync) {
|
|
|
119
140
|
// between npm and the go-server and reintroduced #1544 (0 of 9 Windows
|
|
120
141
|
// installs joined their heartbeat because the two sides disagreed on id).
|
|
121
142
|
try {
|
|
143
|
+
// windowsHide: lo corre un proceso sin consola propia (el emisor del evento de lanzamiento,
|
|
144
|
+
// el setup lanzado por un cliente de IA). Sin la opción, Windows puede mostrar una consola; el
|
|
145
|
+
// cliente Go pone HideWindow por lo mismo (#31). Medido sin ventana en 3.0.417; esto la descarta.
|
|
122
146
|
const ps = execImpl(
|
|
123
147
|
'powershell -NoProfile -Command "(Get-WmiObject Win32_ComputerSystemProduct).UUID"',
|
|
124
|
-
{ stdio: 'pipe' }
|
|
148
|
+
{ stdio: 'pipe', windowsHide: true, timeout: TOPE_PROBE_MS }
|
|
125
149
|
).toString().trim()
|
|
126
150
|
// PowerShell prints the literal string "nil", not an empty line, when WMI
|
|
127
151
|
// has no UUID to give — an emptiness check alone would accept it as an id.
|
|
128
|
-
if (ps
|
|
152
|
+
if (_esIdDeHardwareValido(ps)) {
|
|
129
153
|
_winHardwareIdCache = ps
|
|
130
154
|
return _winHardwareIdCache
|
|
131
155
|
}
|
|
132
156
|
} catch {}
|
|
133
157
|
try {
|
|
134
|
-
const wmic = execImpl('wmic csproduct get UUID /value', { stdio: 'pipe' }).toString()
|
|
158
|
+
const wmic = execImpl('wmic csproduct get UUID /value', { stdio: 'pipe', windowsHide: true, timeout: TOPE_PROBE_MS }).toString()
|
|
135
159
|
const match = wmic.match(/UUID=(.+)/)
|
|
136
|
-
if (match) {
|
|
160
|
+
if (match && _esIdDeHardwareValido(match[1].trim())) {
|
|
137
161
|
_winHardwareIdCache = match[1].trim()
|
|
138
162
|
return _winHardwareIdCache
|
|
139
163
|
}
|
|
@@ -154,6 +178,19 @@ function _resetWinHardwareIdCacheForTests() {
|
|
|
154
178
|
// que invertir la prioridad hardware-antes-que-cache —la decision irreversible de
|
|
155
179
|
// machineid.go— los pasaba a todos en verde. Sin argumentos se comporta igual que
|
|
156
180
|
// antes; ningun caller de produccion pasa deps.
|
|
181
|
+
// The machine_id the macOS tray persisted in its config.json, or '' when there is none yet.
|
|
182
|
+
function _readTrayMachineId(deps = {}) {
|
|
183
|
+
const home = deps.homeDir || require('os').homedir()
|
|
184
|
+
const read = deps.readFileImpl || ((p) => require('fs').readFileSync(p, 'utf8'))
|
|
185
|
+
try {
|
|
186
|
+
const cfg = JSON.parse(read(require('path').join(home, 'Library', 'Application Support', 'Local MCP', 'config.json')))
|
|
187
|
+
const id = typeof cfg.machine_id === 'string' ? cfg.machine_id.trim() : ''
|
|
188
|
+
return id
|
|
189
|
+
} catch {
|
|
190
|
+
return ''
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
157
194
|
function _getMachineId(deps = {}) {
|
|
158
195
|
const platform = deps.platform || process.platform
|
|
159
196
|
const cacheDir = deps.cacheDir || CACHE_DIR
|
|
@@ -165,6 +202,13 @@ function _getMachineId(deps = {}) {
|
|
|
165
202
|
|
|
166
203
|
const execSync = deps.execImpl || require('child_process').execSync
|
|
167
204
|
if (platform === 'darwin') {
|
|
205
|
+
// QA 3.0.418 (Dario, m4): the tray's own id lives in its config.json (it read the keychain
|
|
206
|
+
// when IT could). An npm run in a context without keychain access (an agent or SSH session)
|
|
207
|
+
// fell to the hardware UUID and minted the cloud token for a SECOND identity of the same Mac,
|
|
208
|
+
// so the server refused that Mac's device key, its machine approval and the agent mesh.
|
|
209
|
+
// The id the tray already uses comes first.
|
|
210
|
+
const trayId = _readTrayMachineId(deps)
|
|
211
|
+
if (trayId) return trayId
|
|
168
212
|
try {
|
|
169
213
|
const r = execSync('security find-generic-password -s com.local-mcp.machine-id -a machine-id -w 2>/dev/null', { stdio: 'pipe' })
|
|
170
214
|
const id = r.toString().trim()
|
|
@@ -178,7 +222,7 @@ function _getMachineId(deps = {}) {
|
|
|
178
222
|
} else if (platform === 'win32') {
|
|
179
223
|
const hw = _probeWinHardwareId(execSync)
|
|
180
224
|
if (hw) {
|
|
181
|
-
_writeWinMachineIdCache(cacheDir, hw)
|
|
225
|
+
if (!deps.soloLectura) _writeWinMachineIdCache(cacheDir, hw)
|
|
182
226
|
return hw
|
|
183
227
|
}
|
|
184
228
|
// Both probes failed or are blocked — fall back to whatever the go-server
|
|
@@ -193,6 +237,10 @@ function _getMachineId(deps = {}) {
|
|
|
193
237
|
if (mid) return mid
|
|
194
238
|
} catch {}
|
|
195
239
|
}
|
|
240
|
+
// El evento de lanzamiento (launch-event.js) pide soloLectura: no escribe el cache de arriba —
|
|
241
|
+
// en una máquina desinstalada recrearía el directorio que el usuario borró — y prefiere no
|
|
242
|
+
// tener id antes que uno derivado del hostname.
|
|
243
|
+
if (deps.soloLectura) return ''
|
|
196
244
|
// Fallback: hash hostname instead of sending raw PII
|
|
197
245
|
const crypto = require('crypto')
|
|
198
246
|
return crypto.createHash('sha256').update(os.hostname()).digest('hex').slice(0, 16)
|
|
@@ -247,12 +295,20 @@ async function getLatestBinary() {
|
|
|
247
295
|
let data = ''
|
|
248
296
|
res.on('data', chunk => data += chunk)
|
|
249
297
|
res.on('end', () => {
|
|
298
|
+
let info
|
|
250
299
|
try {
|
|
251
|
-
|
|
252
|
-
resolve(info)
|
|
300
|
+
info = JSON.parse(data)
|
|
253
301
|
} catch (e) {
|
|
254
|
-
reject(new Error(`Respuesta inválida de /runtime/latest: ${data}`))
|
|
302
|
+
return reject(new Error(`Respuesta inválida de /runtime/latest: ${data}`))
|
|
303
|
+
}
|
|
304
|
+
// Un error del API también es JSON (`{"detail": …}` en un 5xx o un 429). Aceptarlo dejaba
|
|
305
|
+
// `version` en undefined y los co-procesos se pedían como `teams-proxy-undefined-…`: 25
|
|
306
|
+
// pedidos en un día de API caído (#38). Sin un 200 con versión no hay respuesta, y todo
|
|
307
|
+
// el que llama ya trata un rechazo como «no disponible».
|
|
308
|
+
if (res.statusCode !== 200 || !info || !/^[0-9][0-9A-Za-z.+-]*$/.test(String(info.version || ''))) {
|
|
309
|
+
return reject(new Error(`/runtime/latest no dio una versión (HTTP ${res.statusCode})`))
|
|
255
310
|
}
|
|
311
|
+
resolve(info)
|
|
256
312
|
})
|
|
257
313
|
})
|
|
258
314
|
req.on('error', reject)
|
|
@@ -331,6 +387,10 @@ async function downloadFile(url, destPath) {
|
|
|
331
387
|
* @returns {Promise<{binPath: string}>}
|
|
332
388
|
*/
|
|
333
389
|
async function ensureBinary(deps = {}) {
|
|
390
|
+
// Linux no está soportado (decisión de Dario, 2026-10-02; colibird-ai/lmcp-npm#9). Se dice
|
|
391
|
+
// ANTES de pedir nada: ni /runtime/latest ni un tarball. Anuncie lo que anuncie el API — llegó a
|
|
392
|
+
// anunciar URLs `linux_*` que daban 404, y la guarda vieja (sólo «si no hay URL») no saltaba.
|
|
393
|
+
if ((deps.platform || process.platform) === 'linux') throw new Error(LINUX_NO_SOPORTADO)
|
|
334
394
|
// Seam for tests (same idiom as ensureTray's `deps`, #2654): lets a test pin the
|
|
335
395
|
// platform and stub the network without touching process.platform or real HOME.
|
|
336
396
|
const {
|
|
@@ -361,17 +421,6 @@ async function ensureBinary(deps = {}) {
|
|
|
361
421
|
const archKey = arch.replace('-', '_') // windows-amd64 → windows_amd64
|
|
362
422
|
const publishedUrl = (info[archKey] && info[archKey].startsWith('http')) ? info[archKey] : null
|
|
363
423
|
|
|
364
|
-
// Linux has no published artifact today — unlike Windows, where windows_amd64
|
|
365
|
-
// IS published — so composing the fallback URL below only ever 404s: 817
|
|
366
|
-
// requests/week of dead traffic from MCP sandboxes testing the npm package
|
|
367
|
-
// (#3794). Fail before any network call instead of catching the 404 after.
|
|
368
|
-
if (platform === 'linux' && !publishedUrl) {
|
|
369
|
-
throw new Error(
|
|
370
|
-
'Local MCP does not support Linux yet.\n' +
|
|
371
|
-
' Check https://local-mcp.com for updates.'
|
|
372
|
-
)
|
|
373
|
-
}
|
|
374
|
-
|
|
375
424
|
let url, platformVersion
|
|
376
425
|
if (publishedUrl) {
|
|
377
426
|
platformVersion = versionFromArtifactUrl(publishedUrl, arch)
|
|
@@ -871,8 +920,8 @@ async function ensureSlackProxy(platform = process.platform) {
|
|
|
871
920
|
module.exports = {
|
|
872
921
|
ensureBinary, ensureRuntime, ensureTray, ensureTeamsProxy, ensureSlackProxy, versionFromArtifactUrl,
|
|
873
922
|
latestVersionForPlatform, getArch,
|
|
874
|
-
CACHE_DIR, TRAY_DIR, _getMachineId,
|
|
875
|
-
_probeWinHardwareId, _resetWinHardwareIdCacheForTests,
|
|
923
|
+
CACHE_DIR, TRAY_DIR, _getMachineId, LINUX_NO_SOPORTADO,
|
|
924
|
+
_probeWinHardwareId, _resetWinHardwareIdCacheForTests, UUIDS_DE_HARDWARE_BASURA, _esIdDeHardwareValido,
|
|
876
925
|
_readWinMachineIdCache, _writeWinMachineIdCache, WIN_MACHINE_ID_CACHE_NAME,
|
|
877
926
|
_retireSupersededTrays, _isUsableTrayBundle, SYSTEM_TRAY_APP,
|
|
878
927
|
_bundleVersion, _isTrayRunningFrom, _versionIsNewer,
|
package/index.js
CHANGED
|
@@ -17,6 +17,7 @@ const path = require('path')
|
|
|
17
17
|
const os = require('os')
|
|
18
18
|
const fs = require('fs')
|
|
19
19
|
const https = require('https')
|
|
20
|
+
const evento = require('./launch-event')
|
|
20
21
|
|
|
21
22
|
// ── Version + liveness helpers (module scope so they are testable) ────────────
|
|
22
23
|
|
|
@@ -232,12 +233,31 @@ function clearUninstallSentinels() {
|
|
|
232
233
|
for (const p of uninstallSentinelPaths()) { try { fs.unlinkSync(p) } catch {} }
|
|
233
234
|
}
|
|
234
235
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
//
|
|
236
|
+
const cmd = process.argv[2]
|
|
237
|
+
|
|
238
|
+
// Comandos que este wrapper entiende. Sin argumentos arranca el server MCP por stdio, que es
|
|
239
|
+
// como lo lanzan los clientes de IA — ese camino NO cambia.
|
|
240
|
+
const COMANDOS = ['setup', 'update', 'uninstall', 'status']
|
|
241
|
+
const PEDIDOS_DE_AYUDA = ['--help', '-h', 'help']
|
|
242
|
+
|
|
243
|
+
// Evento de lanzamiento (launch-event.js, #11): cada lanzamiento del camino del server reporta
|
|
244
|
+
// UNA vez cómo terminó. Un lanzamiento es: sin argumento, `stdio`, o un argumento desconocido.
|
|
245
|
+
// Los comandos y la ayuda no lo son y no reportan. El `exit` de abajo es la red: una salida de
|
|
246
|
+
// ese camino que nadie nombró —una excepción incluida— sale como `other`, nunca en silencio.
|
|
247
|
+
const ES_LANZAMIENTO = require.main === module && !COMANDOS.includes(cmd) && !PEDIDOS_DE_AYUDA.includes(cmd)
|
|
248
|
+
function reportarLanzamiento(outcome, opciones) {
|
|
249
|
+
if (ES_LANZAMIENTO) evento.reportar(outcome, evento.claseDeArgumento(cmd), opciones)
|
|
250
|
+
}
|
|
251
|
+
// Antes que nada: un `spawned` que en Windows esperaba su id sale ahora, por el proceso suelto (#21).
|
|
252
|
+
if (ES_LANZAMIENTO) process.on('exit', () => { evento.soltarPendiente(); reportarLanzamiento('other') })
|
|
253
|
+
|
|
254
|
+
// Platform support: macOS (Swift tray + embedded server) and Windows (go-server standalone
|
|
255
|
+
// binary downloaded from R2). Linux passes this guard so that `--help`, `status`, `update` and
|
|
256
|
+
// `uninstall` answer, but it is NOT supported: the server path and `setup` say so (#9).
|
|
238
257
|
const SUPPORTED_PLATFORMS = ['darwin', 'win32', 'linux']
|
|
239
258
|
if (!SUPPORTED_PLATFORMS.includes(process.platform)) {
|
|
240
|
-
console.error(`LMCP supports macOS
|
|
259
|
+
console.error(`LMCP supports macOS and Windows. Your platform (${process.platform}) is not supported yet — see https://local-mcp.com`)
|
|
260
|
+
reportarLanzamiento('unsupported-platform')
|
|
241
261
|
process.exit(1)
|
|
242
262
|
}
|
|
243
263
|
|
|
@@ -250,13 +270,12 @@ if (parseInt(process.version.slice(1)) < 16) {
|
|
|
250
270
|
`If you use nvm, run: nvm alias default 22 && nvm uninstall ${process.version}\n` +
|
|
251
271
|
'Then restart Claude Desktop / Cursor.'
|
|
252
272
|
process.stderr.write(msg + '\n')
|
|
273
|
+
reportarLanzamiento('unsupported-node')
|
|
253
274
|
// Exit with a non-zero code so the MCP host marks the server as failed
|
|
254
275
|
// (vs hanging forever with no output).
|
|
255
276
|
process.exit(1)
|
|
256
277
|
}
|
|
257
278
|
|
|
258
|
-
const cmd = process.argv[2]
|
|
259
|
-
|
|
260
279
|
// Detect a MANUAL uninstall — the user dragged LocalMCPTray.app to the Trash (by far the
|
|
261
280
|
// most common way to remove a Mac app) instead of running `npx local-mcp uninstall` or the
|
|
262
281
|
// tray's Uninstall button, so no `.uninstalled` sentinel was written. Signature: the tray
|
|
@@ -279,15 +298,18 @@ function detectManualUninstall() {
|
|
|
279
298
|
)
|
|
280
299
|
}
|
|
281
300
|
|
|
282
|
-
//
|
|
283
|
-
//
|
|
284
|
-
|
|
301
|
+
// `stdio` no es un comando: es el modo por defecto dicho en voz alta. Las guías públicas
|
|
302
|
+
// mandaron a escribir `"args": ["-y", "local-mcp@latest", "stdio"]` en la config manual de un
|
|
303
|
+
// cliente de IA, y esas configs siguen en máquinas de usuarios. Tratarlo como desconocido
|
|
304
|
+
// (3.0.413 a 3.0.416) las rompió: ayuda por stdout y exit 2 donde el cliente espera JSON-RPC.
|
|
305
|
+
const SINONIMOS_DEL_SERVER = ['stdio']
|
|
285
306
|
|
|
286
307
|
function mostrarAyuda() {
|
|
287
308
|
console.log(`local-mcp — Local MCP Server
|
|
288
309
|
|
|
289
310
|
Uso:
|
|
290
311
|
local-mcp arranca el server MCP por stdio (lo que hace un cliente de IA)
|
|
312
|
+
local-mcp stdio lo mismo, dicho explícitamente
|
|
291
313
|
local-mcp setup instala y configura
|
|
292
314
|
local-mcp status muestra el estado
|
|
293
315
|
local-mcp update limpia el cache del runtime
|
|
@@ -303,11 +325,12 @@ async function main() {
|
|
|
303
325
|
// sólo buscaba ayuda de CLI. Un comando de consulta no instala software.
|
|
304
326
|
//
|
|
305
327
|
// Sin argumentos sigue arrancando el server, que es como lo invocan los clientes de IA.
|
|
306
|
-
if (cmd !== undefined && !COMANDOS.includes(cmd)) {
|
|
307
|
-
const pedidoDeAyuda =
|
|
328
|
+
if (cmd !== undefined && !COMANDOS.includes(cmd) && !SINONIMOS_DEL_SERVER.includes(cmd)) {
|
|
329
|
+
const pedidoDeAyuda = PEDIDOS_DE_AYUDA.includes(cmd)
|
|
308
330
|
mostrarAyuda()
|
|
309
331
|
if (!pedidoDeAyuda) {
|
|
310
332
|
console.error(`\nNo conozco el comando "${cmd}".`)
|
|
333
|
+
reportarLanzamiento('rejected-argument')
|
|
311
334
|
}
|
|
312
335
|
process.exit(pedidoDeAyuda ? 0 : 2)
|
|
313
336
|
}
|
|
@@ -317,7 +340,8 @@ async function main() {
|
|
|
317
340
|
clearUninstallSentinels()
|
|
318
341
|
const { runSetup } = require('./setup')
|
|
319
342
|
await runSetup()
|
|
320
|
-
|
|
343
|
+
// En Linux el setup no instaló nada (#9): quien lo encadena en un script tiene que verlo.
|
|
344
|
+
process.exit(process.platform === 'linux' ? 1 : 0)
|
|
321
345
|
}
|
|
322
346
|
|
|
323
347
|
if (cmd === 'update') {
|
|
@@ -381,7 +405,6 @@ async function main() {
|
|
|
381
405
|
try {
|
|
382
406
|
const s = JSON.parse(data)
|
|
383
407
|
console.log(`LMCP v${s.version} — ${s.server}`)
|
|
384
|
-
console.log(`Licencia: ${s.license?.status} (${s.license?.days_left}d)`)
|
|
385
408
|
} catch { console.log(data) }
|
|
386
409
|
})
|
|
387
410
|
}).on('error', () => console.log('Servidor no disponible en localhost:8765'))
|
|
@@ -402,6 +425,7 @@ async function main() {
|
|
|
402
425
|
// runtime-dir and durable App Support copies (see uninstallSentinelPaths).
|
|
403
426
|
{
|
|
404
427
|
if (anyUninstallSentinel()) {
|
|
428
|
+
reportarLanzamiento('uninstalled')
|
|
405
429
|
process.stderr.write(
|
|
406
430
|
'[LMCP] Local MCP was uninstalled — not starting.\n' +
|
|
407
431
|
' Reinstall: npx local-mcp setup\n' +
|
|
@@ -421,6 +445,7 @@ async function main() {
|
|
|
421
445
|
// and we short-circuit here, which is harmless.
|
|
422
446
|
{
|
|
423
447
|
if (detectManualUninstall()) {
|
|
448
|
+
reportarLanzamiento('uninstalled')
|
|
424
449
|
writeUninstallSentinels()
|
|
425
450
|
const trayPlist = path.join(os.homedir(), 'Library', 'LaunchAgents', 'com.local-mcp.tray.plist')
|
|
426
451
|
if (fs.existsSync(trayPlist)) {
|
|
@@ -435,6 +460,15 @@ async function main() {
|
|
|
435
460
|
}
|
|
436
461
|
}
|
|
437
462
|
|
|
463
|
+
// ── Linux no está soportado (#9) ────────────────────────────────────────────
|
|
464
|
+
// Se dice acá, antes de cualquier descarga: sin esto el wrapper pedía un binario que no se
|
|
465
|
+
// publica y terminaba en un 404. Va después de los chequeos de desinstalación, que no bajan nada.
|
|
466
|
+
if (process.platform === 'linux') {
|
|
467
|
+
process.stderr.write(require('./download').LINUX_NO_SOPORTADO + '\n')
|
|
468
|
+
reportarLanzamiento('unsupported-platform')
|
|
469
|
+
process.exit(1)
|
|
470
|
+
}
|
|
471
|
+
|
|
438
472
|
// ── Modo default: stdio MCP server ──────────────────────────────────────────
|
|
439
473
|
const { CACHE_DIR } = require('./download')
|
|
440
474
|
// macOS ships the Swift `local-mcp-server` (standalone fallback to the embedded
|
|
@@ -526,7 +560,10 @@ async function main() {
|
|
|
526
560
|
try { _pkgVer = require('./package.json').version || '' } catch {}
|
|
527
561
|
const child = spawn(binaryPath, spawnArgs, { stdio: 'inherit', env: { ...process.env, LMCP_NPM_VERSION: _pkgVer } })
|
|
528
562
|
noteSpawnedServer(child.pid)
|
|
563
|
+
// El server ya arrancó y este proceso queda vivo toda la sesión: el evento sale desde acá.
|
|
564
|
+
child.on('spawn', () => reportarLanzamiento('spawned', { enProceso: true }))
|
|
529
565
|
child.on('error', (err) => {
|
|
566
|
+
reportarLanzamiento('spawn-failed')
|
|
530
567
|
process.stderr.write(`Error ejecutando LMCP: ${err.message}\n`)
|
|
531
568
|
process.exit(1)
|
|
532
569
|
})
|
|
@@ -667,6 +704,7 @@ async function main() {
|
|
|
667
704
|
try {
|
|
668
705
|
runtime = await ensureRuntime()
|
|
669
706
|
} catch (err) {
|
|
707
|
+
reportarLanzamiento('download-failed')
|
|
670
708
|
process.stderr.write(`\nError al preparar el runtime de LMCP:\n${err.message}\n`)
|
|
671
709
|
process.stderr.write(`Intentá reinstalar: npx local-mcp update\n\n`)
|
|
672
710
|
process.exit(1)
|
package/launch-event.js
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict'
|
|
3
|
+
// launch-event.js — el evento de lanzamiento del wrapper (colibird-ai/lmcp-npm#11, parte C).
|
|
4
|
+
//
|
|
5
|
+
// Por qué existe: de 3.0.413 a 3.0.416 el wrapper rechazó `stdio` y ninguna config manual
|
|
6
|
+
// conectaba. Nadie lo vio en diez días: el wrapper salía antes de que existiera telemetría.
|
|
7
|
+
//
|
|
8
|
+
// Contrato: contracts/launch-telemetry.v1.json de lmcp-api (copia fijada en npm/contracts/, que
|
|
9
|
+
// NO se publica: por eso los conjuntos están acá como constantes, y un test los compara).
|
|
10
|
+
//
|
|
11
|
+
// Corre en cada arranque de cada cliente de IA. Lo que no puede pasar nunca:
|
|
12
|
+
// · escribir en stdout — es el protocolo;
|
|
13
|
+
// · demorar el arranque del server o una salida — sin reintentos, timeout corto, todo error
|
|
14
|
+
// tragado;
|
|
15
|
+
// · mandar algo que no sea uno de los cinco campos: ni el argumento, ni rutas, ni entorno.
|
|
16
|
+
const https = require('https')
|
|
17
|
+
const cp = require('child_process')
|
|
18
|
+
const DESTINO = { hostname: 'office-mcp-production.up.railway.app', path: '/launch-event' }
|
|
19
|
+
const PLATAFORMAS = ['darwin', 'win32', 'linux', 'other']
|
|
20
|
+
const CLASES = ['none', 'stdio', 'unknown']
|
|
21
|
+
const OUTCOMES = ['spawned', 'rejected-argument', 'unsupported-node', 'unsupported-platform',
|
|
22
|
+
'download-failed', 'spawn-failed', 'uninstalled', 'other']
|
|
23
|
+
const RE_VERSION = /^\d{1,3}\.\d{1,3}\.\d{1,5}$/
|
|
24
|
+
const RE_MAQUINA = /^[A-Za-z0-9._:{}-]{1,64}$/
|
|
25
|
+
const [TIMEOUT_MS, TOPE_MS] = [3000, 4000] // el pedido · la vida del proceso suelto
|
|
26
|
+
const TOPE_ID_MS = 2000 // Windows: cuánto puede esperar el `spawned` al id de hardware (#21)
|
|
27
|
+
|
|
28
|
+
// Del argumento sólo viaja su clase. `stdio` es el único que se nombra: es el del incidente.
|
|
29
|
+
function claseDeArgumento (cmd) {
|
|
30
|
+
if (cmd === undefined) return 'none'
|
|
31
|
+
return cmd === 'stdio' ? 'stdio' : 'unknown'
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// El heartbeat no tiene compuerta de consentimiento (ver #11). Este evento respeta además el
|
|
35
|
+
// único interruptor que ya existe en el server de Mac.
|
|
36
|
+
function apagado (env = process.env) {
|
|
37
|
+
return ['1', 'true', 'yes'].includes(String(env.LMCP_DISABLE_TELEMETRY || '').toLowerCase())
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// El id que usa el heartbeat, sin escribir nada y sin el fallback del hostname. Lo que no
|
|
41
|
+
// calza con el patrón del contrato (una ruta en LMCP_MACHINE_ID, por ejemplo) no viaja.
|
|
42
|
+
function idDeMaquina () {
|
|
43
|
+
try {
|
|
44
|
+
const id = require('./download')._getMachineId({ soloLectura: true })
|
|
45
|
+
return RE_MAQUINA.test(id) ? id : undefined
|
|
46
|
+
} catch { return undefined }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// null = no se manda. Sólo salen valores de los conjuntos del contrato.
|
|
50
|
+
function cuerpo (outcome, clase, deps = {}) {
|
|
51
|
+
const version = 'version' in deps ? deps.version : require('./package.json').version
|
|
52
|
+
const plataforma = deps.platform || process.platform
|
|
53
|
+
if (!OUTCOMES.includes(outcome) || !CLASES.includes(clase) || !RE_VERSION.test(version)) return null
|
|
54
|
+
const c = {
|
|
55
|
+
package_version: version,
|
|
56
|
+
platform: PLATAFORMAS.includes(plataforma) ? plataforma : 'other',
|
|
57
|
+
arg_class: clase,
|
|
58
|
+
outcome
|
|
59
|
+
}
|
|
60
|
+
const id = 'machineId' in deps ? deps.machineId : idDeMaquina()
|
|
61
|
+
if (typeof id === 'string' && RE_MAQUINA.test(id)) c.machine_id = id
|
|
62
|
+
return c
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Un POST, sin reintento, respuesta ignorada. `soltar` es para el pedido que sale desde el
|
|
66
|
+
// wrapper vivo: el socket no retiene al proceso.
|
|
67
|
+
function enviar (c, { soltar = false } = {}) {
|
|
68
|
+
try {
|
|
69
|
+
const datos = JSON.stringify(c)
|
|
70
|
+
const req = https.request({
|
|
71
|
+
...DESTINO,
|
|
72
|
+
method: 'POST',
|
|
73
|
+
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(datos) },
|
|
74
|
+
timeout: TIMEOUT_MS
|
|
75
|
+
}, (res) => res.resume())
|
|
76
|
+
req.on('error', () => {})
|
|
77
|
+
req.on('timeout', () => { try { req.destroy() } catch {} })
|
|
78
|
+
if (soltar) req.on('socket', (s) => { try { s.unref() } catch {} })
|
|
79
|
+
req.end(datos)
|
|
80
|
+
} catch {}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Windows, sin bloquear (#21): el mismo id que manda el heartbeat del cliente Go
|
|
84
|
+
// (lmcp-win internal/machineid/probe_windows.go: PowerShell, después wmic, TrimSpace, sin cambiar
|
|
85
|
+
// mayúsculas, sin los ids basura), por execFile y sin shell. Llama a `listo` UNA vez: con el id, o
|
|
86
|
+
// con undefined si los dos fallan, devuelven basura o pasa el tope. Del cache y del hash del
|
|
87
|
+
// hostname del cliente Go no se toma nada: un id rechazado no viaja.
|
|
88
|
+
// Lo que corre después de que reportar() volvió (#42, O2/O3/O5): pasado el tope no se lanza otro
|
|
89
|
+
// probe; el probe vivo se mata al terminar y cuando el wrapper sale (matarProbe); y ningún
|
|
90
|
+
// callback puede tirar, porque una excepción acá mataría al wrapper y con él la sesión MCP.
|
|
91
|
+
let probeVivo = null
|
|
92
|
+
let cancelarProbe = () => {}
|
|
93
|
+
function matarProbe () {
|
|
94
|
+
const p = probeVivo
|
|
95
|
+
probeVivo = null
|
|
96
|
+
try { if (p) p.kill() } catch {}
|
|
97
|
+
}
|
|
98
|
+
function idDeHardwareWindows (listo) {
|
|
99
|
+
let hecho = false
|
|
100
|
+
const fin = (id) => {
|
|
101
|
+
if (hecho) return
|
|
102
|
+
hecho = true
|
|
103
|
+
clearTimeout(tope)
|
|
104
|
+
matarProbe()
|
|
105
|
+
try { listo(id) } catch {}
|
|
106
|
+
}
|
|
107
|
+
const tope = setTimeout(() => fin(undefined), TOPE_ID_MS)
|
|
108
|
+
cancelarProbe = () => { hecho = true; clearTimeout(tope); matarProbe() }
|
|
109
|
+
const opciones = { windowsHide: true, timeout: TOPE_ID_MS }
|
|
110
|
+
const correr = (cmd, args, alResponder) => {
|
|
111
|
+
if (hecho) return
|
|
112
|
+
try {
|
|
113
|
+
probeVivo = cp.execFile(cmd, args, opciones, (err, out) => {
|
|
114
|
+
probeVivo = null
|
|
115
|
+
try { alResponder(err ? '' : String(out)) } catch { fin(undefined) }
|
|
116
|
+
})
|
|
117
|
+
} catch { fin(undefined) }
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
const { _esIdDeHardwareValido: valido } = require('./download')
|
|
121
|
+
correr('powershell', ['-NoProfile', '-Command', '(Get-WmiObject Win32_ComputerSystemProduct).UUID'], (out) => {
|
|
122
|
+
const ps = out.trim()
|
|
123
|
+
if (valido(ps)) return fin(ps)
|
|
124
|
+
correr('wmic', ['csproduct', 'get', 'UUID', '/value'], (out2) => {
|
|
125
|
+
const m = out2.match(/UUID=(.+)/)
|
|
126
|
+
const id = m ? m[1].trim() : ''
|
|
127
|
+
fin(valido(id) ? id : undefined)
|
|
128
|
+
})
|
|
129
|
+
})
|
|
130
|
+
} catch { fin(undefined) }
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Un evento por lanzamiento: la primera llamada decide, las demás no hacen nada.
|
|
134
|
+
// enProceso el server ya arrancó y el wrapper queda vivo toda la sesión (`spawned`): el
|
|
135
|
+
// pedido sale desde acá, sin proceso extra.
|
|
136
|
+
// si no el wrapper está por salir: un proceso suelto manda el evento y el wrapper no
|
|
137
|
+
// lo espera. Sin stdout heredado y, en Windows, sin ventana de consola.
|
|
138
|
+
// En Windows sin LMCP_MACHINE_ID, el `spawned` espera al id sin bloquear (idDeHardwareWindows);
|
|
139
|
+
// si el wrapper sale antes, soltarPendiente() lo manda por el proceso suelto, que busca el id solo.
|
|
140
|
+
// Una persona en una terminal (stdin TTY) no es un cliente de IA lanzando el server: no reporta.
|
|
141
|
+
let reportado = false
|
|
142
|
+
let pendiente = null
|
|
143
|
+
function reportar (outcome, clase, { enProceso = false } = {}) {
|
|
144
|
+
if (reportado) return
|
|
145
|
+
reportado = true
|
|
146
|
+
try {
|
|
147
|
+
if (apagado() || (process.stdin && process.stdin.isTTY)) return
|
|
148
|
+
if (!OUTCOMES.includes(outcome) || !CLASES.includes(clase)) return
|
|
149
|
+
if (enProceso) {
|
|
150
|
+
const idEntorno = (process.env.LMCP_MACHINE_ID || '').trim()
|
|
151
|
+
if (process.platform === 'win32' && !idEntorno) {
|
|
152
|
+
pendiente = { outcome, clase }
|
|
153
|
+
idDeHardwareWindows((id) => {
|
|
154
|
+
if (!pendiente) return // ya salió por el proceso suelto
|
|
155
|
+
pendiente = null
|
|
156
|
+
const c = cuerpo(outcome, clase, { machineId: id })
|
|
157
|
+
if (c) enviar(c, { soltar: true })
|
|
158
|
+
})
|
|
159
|
+
return
|
|
160
|
+
}
|
|
161
|
+
const c = cuerpo(outcome, clase, process.platform === 'win32' ? { machineId: idEntorno } : {})
|
|
162
|
+
if (c) enviar(c, { soltar: true })
|
|
163
|
+
return
|
|
164
|
+
}
|
|
165
|
+
lanzarSuelto(outcome, clase)
|
|
166
|
+
} catch {}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function lanzarSuelto (outcome, clase) {
|
|
170
|
+
const hijo = cp.spawn(process.execPath, [__filename, outcome, clase],
|
|
171
|
+
{ detached: true, stdio: 'ignore', windowsHide: true })
|
|
172
|
+
hijo.on('error', () => {})
|
|
173
|
+
hijo.unref()
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// El wrapper está por salir con el `spawned` todavía esperando su id: sale por el proceso suelto,
|
|
177
|
+
// y el probe que seguía vivo se mata (el timeout de execFile vive en el wrapper y se va con él).
|
|
178
|
+
function soltarPendiente () {
|
|
179
|
+
cancelarProbe()
|
|
180
|
+
if (!pendiente) return
|
|
181
|
+
const { outcome, clase } = pendiente
|
|
182
|
+
pendiente = null
|
|
183
|
+
try { lanzarSuelto(outcome, clase) } catch {}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Como programa es el proceso suelto: `node launch-event.js <outcome> <clase>`.
|
|
187
|
+
if (require.main === module) {
|
|
188
|
+
setTimeout(() => process.exit(0), TOPE_MS).unref()
|
|
189
|
+
const c = apagado() ? null : cuerpo(process.argv[2], process.argv[3])
|
|
190
|
+
if (c) enviar(c)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
module.exports = { PLATAFORMAS, CLASES, OUTCOMES, RE_VERSION, RE_MAQUINA, TOPE_ID_MS, claseDeArgumento, apagado, cuerpo, reportar, soltarPendiente,
|
|
194
|
+
_idDeHardwareWindows: idDeHardwareWindows } // costura de test
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "local-mcp",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.418",
|
|
4
4
|
"description": "Let ChatGPT, Claude, Cursor & any MCP client actually use your Mac or Windows PC — read & reply to email, manage your calendar, text over iMessage, find files, work with Teams, Slack & Office. Runs locally, no API keys, free.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"index.js",
|
|
15
15
|
"download.js",
|
|
16
16
|
"setup.js",
|
|
17
|
+
"launch-event.js",
|
|
17
18
|
"postinstall.js",
|
|
18
19
|
"README.md"
|
|
19
20
|
],
|
|
@@ -68,10 +69,10 @@
|
|
|
68
69
|
"homepage": "https://local-mcp.com",
|
|
69
70
|
"repository": {
|
|
70
71
|
"type": "git",
|
|
71
|
-
"url": "https://github.com/
|
|
72
|
+
"url": "https://github.com/colibird-ai/local-mcp-releases.git"
|
|
72
73
|
},
|
|
73
74
|
"bugs": {
|
|
74
|
-
"url": "https://github.com/
|
|
75
|
+
"url": "https://github.com/colibird-ai/local-mcp-releases/issues"
|
|
75
76
|
},
|
|
76
77
|
"mcpName": "com.local-mcp/local-mcp"
|
|
77
78
|
}
|
package/setup.js
CHANGED
|
@@ -107,7 +107,7 @@ function _localMcpConfigDir(platform = process.platform, home = HOME, appData =
|
|
|
107
107
|
|
|
108
108
|
function _cmdExists(cmd) {
|
|
109
109
|
const which = _IS_WIN ? 'where' : 'which'
|
|
110
|
-
try { execSync(`${which} ${cmd}`, { stdio: 'pipe' }); return true } catch { return false }
|
|
110
|
+
try { execSync(`${which} ${cmd}`, { stdio: 'pipe', windowsHide: true }); return true } catch { return false }
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
// ── Safe config read ─────────────────────────────────────────────────────────
|
|
@@ -237,11 +237,12 @@ function _launcherArgsForConfigureClients(platform, command, args) {
|
|
|
237
237
|
return platform === 'darwin' ? { command, args } : { command: null, args: [] }
|
|
238
238
|
}
|
|
239
239
|
|
|
240
|
-
// §3.8 — Linux's fixed result shape: no capability, no report, exit 0
|
|
241
|
-
//
|
|
242
|
-
//
|
|
240
|
+
// §3.8 — Linux's fixed result shape: no capability, no report, exit 1. It said exit 0 while
|
|
241
|
+
// Linux was "no writer yet"; since #9 Linux is not supported, and a setup that installed nothing
|
|
242
|
+
// is a failed setup: a script that chains on it has to see it. Extracted so the contract itself
|
|
243
|
+
// is directly testable instead of only reachable by reading runSetup's source.
|
|
243
244
|
function _notSupportedOnPlatformResult() {
|
|
244
|
-
return { runtime: 'not_supported_on_platform', exitCode:
|
|
245
|
+
return { runtime: 'not_supported_on_platform', exitCode: 1, report: null }
|
|
245
246
|
}
|
|
246
247
|
|
|
247
248
|
// Corre `--configure-clients --json`. Nunca cuelga el instalador: un binario que no responde
|
|
@@ -253,7 +254,9 @@ function runConfigureClients(bin, command, args, opts = {}) {
|
|
|
253
254
|
if (!bin) return { runtime: 'no_runtime', exitCode: 2, report: null }
|
|
254
255
|
const argv = ['--configure-clients', '--json']
|
|
255
256
|
if (command) argv.push('--command=' + command, ...(args || []).map(a => '--arg=' + a))
|
|
256
|
-
|
|
257
|
+
// windowsHide en los tres procesos que setup arranca en Windows (#31): todos capturan su
|
|
258
|
+
// salida, ninguno es para mirar, y setup puede correr sin consola (un agente, postinstall).
|
|
259
|
+
const r = spawn(bin, argv, { input: '', timeout: timeoutMs, encoding: 'utf8', windowsHide: true })
|
|
257
260
|
if (r.error && r.error.code === 'ETIMEDOUT') return { runtime: 'timeout', exitCode: 2, report: null }
|
|
258
261
|
if (r.error) return { runtime: 'failed', reason: r.error.message, exitCode: 2, report: null }
|
|
259
262
|
let report = null
|
|
@@ -296,10 +299,27 @@ async function runSetup(opts = {}) {
|
|
|
296
299
|
// #3212 — `--json`: el reporte estructurado va solo por stdout; todo lo demás, a stderr.
|
|
297
300
|
const jsonMode = !!opts.json || process.argv.includes('--json')
|
|
298
301
|
if (jsonMode) console.log = (...a) => { process.stderr.write(a.join(' ') + '\n') }
|
|
299
|
-
//
|
|
300
|
-
//
|
|
301
|
-
//
|
|
302
|
-
|
|
302
|
+
// Linux no está soportado (#9): se dice y no se toca nada — sin descarga, sin launcher, sin
|
|
303
|
+
// eventos de instalación ni heartbeat (antes terminaba en «LMCP is installed and running»
|
|
304
|
+
// después de un 404). El reporte --json conserva su forma (§3.8); el código de salida es 1.
|
|
305
|
+
if (process.platform === 'linux') {
|
|
306
|
+
const sinLinux = _notSupportedOnPlatformResult()
|
|
307
|
+
console.log('\n ' + require('./download').LINUX_NO_SOPORTADO + '\n Nothing was installed.\n')
|
|
308
|
+
if (jsonMode) {
|
|
309
|
+
process.stdout.write(JSON.stringify({
|
|
310
|
+
schema: CLIENTS_REPORT_SCHEMA,
|
|
311
|
+
installer_version: require('./package.json').version,
|
|
312
|
+
runtime: sinLinux.runtime,
|
|
313
|
+
catalog: null,
|
|
314
|
+
clients: [],
|
|
315
|
+
permissions: 'unknown_until_tray_runs',
|
|
316
|
+
}) + '\n')
|
|
317
|
+
}
|
|
318
|
+
process.exitCode = sinLinux.exitCode
|
|
319
|
+
return
|
|
320
|
+
}
|
|
321
|
+
// Cross-platform: macOS configures the Swift tray + embedded server; Windows downloads the
|
|
322
|
+
// go-server standalone binary (download.js) and writes the MCP client config.
|
|
303
323
|
const forceAll = opts.all || process.argv.includes('--all')
|
|
304
324
|
const refArg = process.argv.find(a => a.startsWith('--ref='))
|
|
305
325
|
if (refArg) process.env.LMCP_REF = refArg.split('=')[1]
|
|
@@ -469,16 +489,6 @@ async function runSetup(opts = {}) {
|
|
|
469
489
|
console.log(' npx local-mcp setup\n')
|
|
470
490
|
console.log(' Full setup guide: https://local-mcp.com/setup')
|
|
471
491
|
}
|
|
472
|
-
} else {
|
|
473
|
-
// Linux (§3.8): no runtime-native config writer exists yet. Say so plainly,
|
|
474
|
-
// point at the manual guide, and touch nothing — no per-client guessing,
|
|
475
|
-
// no files written, no client identity hardcoded here.
|
|
476
|
-
clientsRun = _notSupportedOnPlatformResult()
|
|
477
|
-
_trackSetupStep('detect', 'not_supported_on_platform', '', { npmVersion: _npmPkgVersion })
|
|
478
|
-
console.log('\n LMCP is installed and running.')
|
|
479
|
-
console.log(' Automatic AI-client configuration is not available on Linux yet.')
|
|
480
|
-
console.log(' Add LMCP to your AI client\'s MCP config manually — see:')
|
|
481
|
-
console.log(' https://local-mcp.com/setup\n')
|
|
482
492
|
}
|
|
483
493
|
|
|
484
494
|
const cfgDir = _localMcpConfigDir()
|
|
@@ -613,8 +623,8 @@ async function runSetup(opts = {}) {
|
|
|
613
623
|
// Anon-first cloud relay activation (replaces the interactive email prompt
|
|
614
624
|
// removed 2026-04-19). When no email is set, register an anonymous tunnel
|
|
615
625
|
// token keyed on machine_id so cloud relay comes online immediately. Users
|
|
616
|
-
// can attach an email later via the tray or /settings
|
|
617
|
-
// sync
|
|
626
|
+
// can attach an email later via the tray or /settings for cross-Mac
|
|
627
|
+
// sync. Non-fatal on failure — the install still
|
|
618
628
|
// succeeds and the server can retry at startup.
|
|
619
629
|
// Provisioning is ALWAYS anonymous, with or without an email — same change made in install.sh
|
|
620
630
|
// (#1083). Two separate ways this used to be skipped, and both left the machine with NO
|
|
@@ -678,7 +688,7 @@ async function runSetup(opts = {}) {
|
|
|
678
688
|
|
|
679
689
|
// #3212 — el reporte para quien instaló sin mirar (un agente): qué quedó hecho y qué espera
|
|
680
690
|
// a la persona. Los permisos, sólo si los midió el tray. clientsRun is always set by this
|
|
681
|
-
// point — darwin/win32 from runConfigureClients
|
|
691
|
+
// point — darwin/win32 from runConfigureClients (linux returned at the top, #9).
|
|
682
692
|
if (jsonMode) {
|
|
683
693
|
const rep = clientsRun.report || {}
|
|
684
694
|
const out = {
|
|
@@ -747,6 +757,7 @@ function _runHealthCheck() {
|
|
|
747
757
|
const result = execFileSync(binPath, args, {
|
|
748
758
|
timeout: 10000,
|
|
749
759
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
760
|
+
windowsHide: true,
|
|
750
761
|
env: { ...process.env, HOME },
|
|
751
762
|
})
|
|
752
763
|
const output = result.toString().trim()
|
|
@@ -1183,6 +1194,7 @@ async function _autoLaunchClient(clientsRun, opts = {}) {
|
|
|
1183
1194
|
|
|
1184
1195
|
module.exports = {
|
|
1185
1196
|
runSetup, runConfigureClients, _findServerBinary, _launcherArgsForConfigureClients,
|
|
1197
|
+
_cmdExists, _runHealthCheck,
|
|
1186
1198
|
CLIENTS_REPORT_SCHEMA,
|
|
1187
1199
|
_notSupportedOnPlatformResult,
|
|
1188
1200
|
_launcherSpawnSpec, _writeLaunchScript, _writeLaunchSpec,
|