local-mcp 3.0.113 → 3.0.115

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.
Files changed (4) hide show
  1. package/download.js +47 -1
  2. package/index.js +44 -38
  3. package/package.json +1 -1
  4. package/setup.js +3 -16
package/download.js CHANGED
@@ -439,4 +439,50 @@ async function ensureHelper() {
439
439
  }
440
440
  }
441
441
 
442
- module.exports = { ensureBinary, ensureRuntime, ensureTray, ensureTeamsProxy, ensureSlackProxy, ensureHelper, CACHE_DIR, TRAY_DIR }
442
+ /**
443
+ * Downloads local-mcp-jxa-runner on first install only.
444
+ * CRITICAL: never replaces an existing jxa-runner binary — the runner owns
445
+ * Automation (JXA) TCC grants for Mail, Messages, Safari, etc. via its cdhash.
446
+ * Replacing the binary silently resets those grants and users would have to
447
+ * re-authorize in System Settings → Privacy & Security → Automation.
448
+ * @returns {Promise<string|null>} Path to the binary, or null if it fails
449
+ */
450
+ async function ensureJXARunner() {
451
+ const binPath = path.join(CACHE_DIR, 'local-mcp-jxa-runner')
452
+
453
+ // Already installed — never replace. Stable cdhash = stable TCC grants.
454
+ if (fs.existsSync(binPath)) return binPath
455
+
456
+ const info = await getLatestBinary()
457
+ const version = info.version
458
+ const arch = process.arch === 'arm64' ? 'darwin-arm64' : 'darwin-x64'
459
+ const url = `https://download.local-mcp.com/local-mcp-jxa-runner-${version}-${arch}.tar.gz`
460
+ process.stderr.write(`\nDownloading local-mcp-jxa-runner v${version}...\n`)
461
+
462
+ fs.mkdirSync(CACHE_DIR, { recursive: true })
463
+ const tarPath = path.join(CACHE_DIR, `local-mcp-jxa-runner-${version}.tar.gz`)
464
+
465
+ try {
466
+ await downloadFile(url, tarPath)
467
+ const stat = fs.statSync(tarPath)
468
+ if (stat.size < 1000) { fs.unlinkSync(tarPath); throw new Error(`Incomplete download (${stat.size} bytes)`) }
469
+
470
+ execFileSync('tar', ['-xzf', tarPath, '-C', CACHE_DIR], { stdio: 'pipe' })
471
+ fs.unlinkSync(tarPath)
472
+
473
+ if (!fs.existsSync(binPath)) throw new Error('local-mcp-jxa-runner not found after extraction')
474
+ fs.chmodSync(binPath, 0o755)
475
+ // Do NOT codesign: the pre-built binary's cdhash must remain exactly as
476
+ // distributed so the Automation TCC entry stays valid permanently.
477
+
478
+ fs.writeFileSync(path.join(CACHE_DIR, '.jxa-runner-version'), version, 'utf8')
479
+ process.stderr.write(` local-mcp-jxa-runner installed at ${binPath}\n`)
480
+ return binPath
481
+ } catch (err) {
482
+ try { if (fs.existsSync(tarPath)) fs.unlinkSync(tarPath) } catch {}
483
+ process.stderr.write(` (local-mcp-jxa-runner not available: ${err.message})\n`)
484
+ return null
485
+ }
486
+ }
487
+
488
+ module.exports = { ensureBinary, ensureRuntime, ensureTray, ensureTeamsProxy, ensureSlackProxy, ensureHelper, ensureJXARunner, CACHE_DIR, TRAY_DIR }
package/index.js CHANGED
@@ -17,39 +17,6 @@ const os = require('os')
17
17
  const fs = require('fs')
18
18
  const https = require('https')
19
19
 
20
- // Track first run / version change so analytics catch users who launch
21
- // via `npx local-mcp` without going through `setup` (manual MCP config).
22
- function _pingFirstRun(versionChanged) {
23
- try {
24
- const pkg = require('./package.json')
25
- const machineId = (() => {
26
- try {
27
- const out = execSync("system_profiler SPHardwareDataType 2>/dev/null | awk '/Hardware UUID/ { print $3 }'", { encoding: 'utf8' }).trim()
28
- return out || ''
29
- } catch { return '' }
30
- })()
31
- const data = JSON.stringify({
32
- version: pkg.version,
33
- os_version: os.release(),
34
- arch: process.arch,
35
- node_version: process.version,
36
- method: 'npx-default',
37
- machine_id: machineId,
38
- source: process.env.LMCP_SOURCE || '',
39
- ref: process.env.LMCP_REF || '',
40
- })
41
- const req = https.request({
42
- hostname: 'office-mcp-production.up.railway.app',
43
- path: '/install/npm',
44
- method: 'POST',
45
- headers: { 'Content-Type': 'application/json', 'Content-Length': data.length },
46
- timeout: 5000,
47
- })
48
- req.on('error', () => {})
49
- req.write(data)
50
- req.end()
51
- } catch { /* fire and forget */ }
52
- }
53
20
 
54
21
  // Solo macOS
55
22
  if (process.platform !== 'darwin') {
@@ -136,21 +103,59 @@ async function main() {
136
103
  : ''
137
104
  if (cachedVersion && semverGte(cachedVersion, pkg.version)) {
138
105
  // Cached binary is same or newer — use fast path, no download needed
139
- const { ensureTray, ensureTeamsProxy, ensureHelper } = require('./download')
106
+ const { ensureTray, ensureTeamsProxy, ensureHelper, ensureJXARunner } = require('./download')
140
107
  ensureTray().catch(() => {})
141
108
  ensureTeamsProxy().catch(() => {})
142
109
  ensureHelper().catch(() => {})
110
+ ensureJXARunner().catch(() => {})
143
111
  const child = spawn(stableBin, [], { stdio: 'inherit', env: process.env })
144
112
  child.on('error', () => process.exit(1))
145
113
  child.on('exit', (code) => process.exit(code ?? 0))
114
+
115
+ // Background update check — fires 10s after startup, at most once per 24h.
116
+ // Covers machines where the tray is dead and Sparkle can't update the binary.
117
+ const lastCheckFile = path.join(CACHE_DIR, '.last-update-check')
118
+ function _needsUpdateCheck() {
119
+ try { return Date.now() - parseInt(fs.readFileSync(lastCheckFile, 'utf8').trim(), 10) > 86400000 } catch { return true }
120
+ }
121
+ if (_needsUpdateCheck()) {
122
+ try { fs.writeFileSync(lastCheckFile, String(Date.now())) } catch {}
123
+ const _cachedVersion = cachedVersion // capture for closure
124
+ setTimeout(() => {
125
+ const req = https.get('https://office-mcp-production.up.railway.app/runtime/latest', { timeout: 8000 }, (res) => {
126
+ let buf = ''
127
+ res.on('data', c => buf += c)
128
+ res.on('end', () => {
129
+ try {
130
+ const { version: latestVersion } = JSON.parse(buf)
131
+ if (latestVersion && !semverGte(_cachedVersion, latestVersion)) {
132
+ // New version available — download silently for next restart
133
+ const { ensureRuntime } = require('./download')
134
+ ensureRuntime().then(({ binPath, version: downloadedVersion }) => {
135
+ try {
136
+ const tmpPath = stableBin + '.tmp'
137
+ fs.copyFileSync(binPath, tmpPath)
138
+ fs.chmodSync(tmpPath, 0o755)
139
+ try { execFileSync('codesign', ['--force', '--sign', '-', '--identifier', 'com.local-mcp.server', tmpPath], { stdio: 'pipe' }) } catch {}
140
+ fs.renameSync(tmpPath, stableBin)
141
+ fs.writeFileSync(versionFile, downloadedVersion || latestVersion)
142
+ } catch {}
143
+ }).catch(() => {})
144
+ }
145
+ } catch {}
146
+ })
147
+ })
148
+ req.on('error', () => {})
149
+ req.on('timeout', () => { try { req.destroy() } catch {} })
150
+ }, 10000)
151
+ }
152
+
146
153
  return
147
154
  }
148
155
  // Cached binary is older than our npm package — download/update
149
156
  process.stderr.write(`Updating LMCP server (${cachedVersion || '?'} → ${pkg.version})...\n`)
150
- _pingFirstRun(true)
151
157
  } else {
152
158
  // No cached binary version → first run via default mode
153
- _pingFirstRun(false)
154
159
  }
155
160
  if (stat.isSymbolicLink()) fs.unlinkSync(stableBin)
156
161
  } catch {
@@ -158,12 +163,13 @@ async function main() {
158
163
  }
159
164
 
160
165
  // Slow path: download/repair binary
161
- const { ensureRuntime, ensureTray, ensureTeamsProxy, ensureHelper } = require('./download')
166
+ const { ensureRuntime, ensureTray, ensureTeamsProxy, ensureHelper, ensureJXARunner } = require('./download')
162
167
 
163
- // Update tray + proxies in background. Helper is downloaded only on first install.
168
+ // Update tray + proxies in background. Helper + jxa-runner downloaded only on first install.
164
169
  ensureTray().catch(() => {})
165
170
  ensureTeamsProxy().catch(() => {})
166
171
  ensureHelper().catch(() => {})
172
+ ensureJXARunner().catch(() => {})
167
173
 
168
174
  let runtime
169
175
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "local-mcp",
3
- "version": "3.0.113",
3
+ "version": "3.0.115",
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": {
package/setup.js CHANGED
@@ -327,7 +327,9 @@ async function runSetup(opts = {}) {
327
327
  } catch { /* non-fatal */ }
328
328
  }
329
329
 
330
- _pingInstall(configured, opts.method || 'setup', email)
330
+ // LMCP_METHOD lets install.sh pass 'curl' so we can distinguish it from
331
+ // a direct 'npx local-mcp setup' invocation in the analytics.
332
+ _pingInstall(configured, process.env.LMCP_METHOD || opts.method || 'npx-setup', email)
331
333
  }
332
334
 
333
335
  async function _installTeamsProxy() {
@@ -576,21 +578,6 @@ function _autoLaunchClient(configured) {
576
578
  return
577
579
  }
578
580
 
579
- // Log auto-launch event (fire-and-forget)
580
- try {
581
- const https = require('https')
582
- const data = JSON.stringify({ event: 'install_auto_launch', client: clientName })
583
- const req = https.request({
584
- hostname: BACKEND_HOST,
585
- path: '/install-event',
586
- method: 'POST',
587
- headers: { 'Content-Type': 'application/json', 'Content-Length': data.length },
588
- timeout: 5000,
589
- })
590
- req.on('error', () => {})
591
- req.write(data)
592
- req.end()
593
- } catch {}
594
581
  }
595
582
 
596
583
  module.exports = { runSetup, injectMcpConfig, CLIENTS }