local-mcp 3.0.126 → 3.0.128

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 (2) hide show
  1. package/package.json +1 -1
  2. package/setup.js +69 -12
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "local-mcp",
3
- "version": "3.0.126",
3
+ "version": "3.0.128",
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
@@ -244,27 +244,60 @@ async function runSetup(opts = {}) {
244
244
  } catch { /* config not created yet — will be created on first run */ }
245
245
  }
246
246
 
247
- // Interactive email prompt — only if TTY and no email from env (LMC-833)
248
- // Lets the activation nudge reach this machine. Optional, non-blocking.
249
- if (!email && process.stdin.isTTY) {
247
+ // Interactive email prompt — uses /dev/tty so it works even from `curl | bash`
248
+ // (stdin is the pipe in that context; /dev/tty is always the real terminal).
249
+ // Non-fatal: if /dev/tty is unavailable (CI, automated scripts) we catch and continue.
250
+ if (!email) {
251
+ let emailPromptResult = 'not_shown_no_tty'
250
252
  try {
251
- const rl = require('readline').createInterface({ input: process.stdin, output: process.stdout })
252
- const ans = await new Promise(res => {
253
- rl.question('\n Email for support & update notifications (optional, press Enter to skip): ', a => {
254
- rl.close()
255
- res((a || '').trim())
256
- })
253
+ const ttyFd = fs.openSync('/dev/tty', 'r+')
254
+ const ttyIn = require('stream').Readable.from(
255
+ (function* () {
256
+ const buf = Buffer.alloc(256)
257
+ let line = ''
258
+ while (true) {
259
+ const n = fs.readSync(ttyFd, buf, 0, 1, null)
260
+ if (n === 0) break
261
+ const ch = buf.slice(0, n).toString()
262
+ if (ch === '\n' || ch === '\r') break
263
+ line += ch
264
+ }
265
+ yield line
266
+ })()
267
+ )
268
+ const ttyOut = new (require('stream').Writable)({
269
+ write(chunk, _enc, cb) { fs.writeSync(ttyFd, chunk); cb() },
257
270
  })
271
+ const rl = require('readline').createInterface({ input: ttyIn, output: ttyOut, terminal: true })
272
+
273
+ process.stdout.write('\n 📧 Email for update notifications (optional, press Enter to skip): ')
274
+ emailPromptResult = 'shown'
275
+ const ans = await Promise.race([
276
+ new Promise(res => {
277
+ rl.once('line', a => { rl.close(); try { fs.closeSync(ttyFd) } catch {} ; res((a || '').trim()) })
278
+ }),
279
+ new Promise(res => setTimeout(() => {
280
+ try { rl.close() } catch {}
281
+ try { fs.closeSync(ttyFd) } catch {}
282
+ res('')
283
+ }, 30000)),
284
+ ])
285
+
258
286
  if (ans && ans.includes('@')) {
259
287
  email = ans
288
+ emailPromptResult = 'submitted'
260
289
  try {
261
290
  const cfg = _readJson(cfgFile)
262
291
  if (!cfg.license_email) { cfg.license_email = email; _writeJson(cfgFile, cfg) }
263
292
  } catch {}
264
- // Email will be sent to the backend on the next heartbeat via license_email
265
- console.log(` ✓ Email saved`)
293
+ console.log(' ✓ Email saved')
294
+ } else {
295
+ emailPromptResult = 'skipped'
266
296
  }
267
- } catch { /* non-fatal install continues without email */ }
297
+ } catch { emailPromptResult = 'failed' }
298
+
299
+ // Track prompt outcome so we can measure conversion and diagnose failures
300
+ _trackEmailPrompt(emailPromptResult)
268
301
  }
269
302
 
270
303
  // Health check — verify binary works before showing success
@@ -501,6 +534,30 @@ function _migrateCurlLaunchAgent() {
501
534
  } catch { /* non-fatal — don't block install on unexpected plist content */ }
502
535
  }
503
536
 
537
+ function _trackEmailPrompt(result) {
538
+ // result: 'shown' | 'submitted' | 'skipped' | 'not_shown_no_tty' | 'failed'
539
+ try {
540
+ const https = require('https')
541
+ const machineId = _getMachineId()
542
+ const data = JSON.stringify({
543
+ stage: 'email_prompt',
544
+ email_prompt_result: result,
545
+ machine_id: machineId,
546
+ install_id: process.env.INSTALL_ID || '',
547
+ })
548
+ const req = https.request({
549
+ hostname: BACKEND_HOST,
550
+ path: '/install-event',
551
+ method: 'POST',
552
+ headers: { 'Content-Type': 'application/json', 'Content-Length': data.length },
553
+ timeout: 5000,
554
+ })
555
+ req.on('error', () => {})
556
+ req.write(data)
557
+ req.end()
558
+ } catch { /* non-fatal */ }
559
+ }
560
+
504
561
  function _pingInstall(clients, method, email = '') {
505
562
  try {
506
563
  const https = require('https')