lantern-connect 0.1.0

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 ADDED
@@ -0,0 +1,90 @@
1
+ # lantern-connect
2
+
3
+ CLI installer to connect AI tools to Lantern.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npx lantern-connect
9
+ ```
10
+
11
+ Or install globally:
12
+
13
+ ```bash
14
+ npm install -g lantern-connect
15
+ lantern-connect
16
+ ```
17
+
18
+ ## Usage
19
+
20
+ ```bash
21
+ # Production (default)
22
+ lantern-connect
23
+
24
+ # Local development with ngrok
25
+ lantern-connect --mcp-url https://your-ngrok-url.ngrok-free.dev/mcp
26
+
27
+ # Custom Lantern instance
28
+ lantern-connect --lantern-url https://your-lantern.com --mcp-url https://api.your-lantern.com/mcp
29
+ ```
30
+
31
+ ## Options
32
+
33
+ | Option | Description | Default |
34
+ |--------|-------------|---------|
35
+ | `--mcp-url <url>` | MCP server URL | `https://api.onlantern.com/mcp` |
36
+ | `--lantern-url <url>` | Lantern web URL for authentication | `https://onlantern.com` |
37
+ | `--help, -h` | Show help message | |
38
+
39
+ ## What it does
40
+
41
+ 1. **Detects** installed AI tools (Claude Desktop, Claude Code, Gemini CLI, Cursor)
42
+ 2. **Authenticates** with your Lantern account via browser
43
+ 3. **Configures** each detected tool to connect to Lantern's MCP server
44
+
45
+ ## Supported Tools
46
+
47
+ | Tool | Config Location | Method |
48
+ |------|-----------------|--------|
49
+ | Claude Desktop | `~/Library/Application Support/Claude/claude_desktop_config.json` | mcp-remote |
50
+ | Claude Code | `~/.claude.json` | `claude mcp add` |
51
+ | Gemini CLI | `~/.gemini/settings.json` | httpUrl config |
52
+ | Cursor | `~/.cursor/mcp.json` | url config |
53
+
54
+ ## After Installation
55
+
56
+ 1. **Fully quit and reopen** your AI tools (Cmd+Q on macOS)
57
+ 2. **Claude Desktop only:** Go to Settings → Connectors → click "lantern" to authenticate
58
+ 3. Start using Lantern:
59
+ - Say "save to lantern" to export a conversation
60
+ - Say "list my lantern conversations" to see your library
61
+ - Say "load my lantern collection" to import a collection
62
+
63
+ ## Local Development
64
+
65
+ ```bash
66
+ # Start your local Lantern backend and frontend
67
+ cd lantern && npm run dev
68
+ cd lantern/frontend && npm run dev
69
+
70
+ # Start ngrok to expose backend (required for Claude Desktop)
71
+ ngrok http 3001
72
+
73
+ # Run the CLI with your ngrok URL
74
+ cd lantern-connect
75
+ node bin/lantern-connect.js --mcp-url https://your-ngrok-url.ngrok-free.dev/mcp --lantern-url http://localhost:3000
76
+ ```
77
+
78
+ ## Troubleshooting
79
+
80
+ ### Claude Desktop shows "Server disconnected"
81
+ - Make sure you're using an ngrok URL (Claude Desktop can't reach localhost)
82
+ - Restart Claude Desktop completely (Cmd+Q, not just close window)
83
+
84
+ ### Gemini CLI shows "Bad Request" for SSE
85
+ - Make sure your backend has the latest MCP session handling code
86
+ - Restart your backend server
87
+
88
+ ### CORS errors on localhost:3000
89
+ - Make sure your frontend's `.env.local` has `NEXT_PUBLIC_API_URL=http://localhost:3001`
90
+ - The `--mcp-url` flag is for the CLI/MCP tools, not the frontend
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { main } from '../src/index.js'
4
+
5
+ main().catch((err) => {
6
+ console.error('\nāŒ Error:', err.message)
7
+ process.exit(1)
8
+ })
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "lantern-connect",
3
+ "version": "0.1.0",
4
+ "description": "CLI installer to connect AI tools (Claude, Gemini, Cursor) to Lantern",
5
+ "type": "module",
6
+ "bin": {
7
+ "lantern-connect": "./bin/lantern-connect.js"
8
+ },
9
+ "files": [
10
+ "bin",
11
+ "src"
12
+ ],
13
+ "scripts": {
14
+ "start": "node bin/lantern-connect.js"
15
+ },
16
+ "keywords": [
17
+ "lantern",
18
+ "mcp",
19
+ "claude",
20
+ "gemini",
21
+ "cursor",
22
+ "ai",
23
+ "cli",
24
+ "model-context-protocol"
25
+ ],
26
+ "author": "Lantern",
27
+ "license": "MIT",
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "https://github.com/onlantern/lantern-connect"
31
+ },
32
+ "homepage": "https://onlantern.com",
33
+ "engines": {
34
+ "node": ">=18.0.0"
35
+ },
36
+ "dependencies": {
37
+ "open": "^10.1.0"
38
+ }
39
+ }
package/src/auth.js ADDED
@@ -0,0 +1,171 @@
1
+ import { createServer } from 'http'
2
+ import { URL } from 'url'
3
+ import open from 'open'
4
+
5
+ const CALLBACK_PORT = 8765
6
+ const DEFAULT_LANTERN_URL = process.env.LANTERN_URL || 'https://onlantern.com'
7
+ const DEFAULT_MCP_URL = process.env.MCP_URL || 'https://api.onlantern.com/mcp'
8
+
9
+ function createSuccessHtml(email) {
10
+ return `
11
+ <!DOCTYPE html>
12
+ <html>
13
+ <head>
14
+ <meta charset="UTF-8">
15
+ <title>Lantern Connected</title>
16
+ <style>
17
+ body {
18
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
19
+ display: flex;
20
+ align-items: center;
21
+ justify-content: center;
22
+ min-height: 100vh;
23
+ margin: 0;
24
+ background: #0a0a0a;
25
+ color: #fafafa;
26
+ }
27
+ .container {
28
+ text-align: center;
29
+ padding: 2rem;
30
+ }
31
+ .icon {
32
+ font-size: 4rem;
33
+ margin-bottom: 1rem;
34
+ }
35
+ h1 {
36
+ font-size: 1.5rem;
37
+ margin-bottom: 0.5rem;
38
+ }
39
+ p {
40
+ color: #a1a1aa;
41
+ margin-bottom: 1.5rem;
42
+ }
43
+ .close-note {
44
+ font-size: 0.875rem;
45
+ color: #71717a;
46
+ }
47
+ </style>
48
+ </head>
49
+ <body>
50
+ <div class="container">
51
+ <div class="icon">āœ“</div>
52
+ <h1>Connected to Lantern</h1>
53
+ <p>Logged in as ${email}</p>
54
+ <p class="close-note">You can close this window and return to your terminal.</p>
55
+ </div>
56
+ </body>
57
+ </html>
58
+ `
59
+ }
60
+
61
+ function createErrorHtml(message) {
62
+ return `
63
+ <!DOCTYPE html>
64
+ <html>
65
+ <head>
66
+ <meta charset="UTF-8">
67
+ <title>Lantern - Error</title>
68
+ <style>
69
+ body {
70
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
71
+ display: flex;
72
+ align-items: center;
73
+ justify-content: center;
74
+ min-height: 100vh;
75
+ margin: 0;
76
+ background: #0a0a0a;
77
+ color: #fafafa;
78
+ }
79
+ .container {
80
+ text-align: center;
81
+ padding: 2rem;
82
+ }
83
+ .icon {
84
+ font-size: 4rem;
85
+ margin-bottom: 1rem;
86
+ }
87
+ h1 {
88
+ font-size: 1.5rem;
89
+ margin-bottom: 0.5rem;
90
+ color: #ef4444;
91
+ }
92
+ p {
93
+ color: #a1a1aa;
94
+ }
95
+ </style>
96
+ </head>
97
+ <body>
98
+ <div class="container">
99
+ <div class="icon">āœ—</div>
100
+ <h1>Connection Failed</h1>
101
+ <p>${message}</p>
102
+ </div>
103
+ </body>
104
+ </html>
105
+ `
106
+ }
107
+
108
+ export async function authenticate(options = {}) {
109
+ const lanternUrl = options.lanternUrl || DEFAULT_LANTERN_URL
110
+ const mcpUrlOverride = options.mcpUrl || DEFAULT_MCP_URL
111
+
112
+ return new Promise((resolve, reject) => {
113
+ const server = createServer((req, res) => {
114
+ const url = new URL(req.url, `http://localhost:${CALLBACK_PORT}`)
115
+
116
+ if (url.pathname === '/callback') {
117
+ const token = url.searchParams.get('token')
118
+ const mcpUrl = url.searchParams.get('mcp_url') || mcpUrlOverride
119
+ const email = url.searchParams.get('email')
120
+ const error = url.searchParams.get('error')
121
+
122
+ if (error) {
123
+ res.writeHead(200, { 'Content-Type': 'text/html' })
124
+ res.end(createErrorHtml(error))
125
+ server.close()
126
+ reject(new Error(error))
127
+ return
128
+ }
129
+
130
+ if (!token) {
131
+ res.writeHead(200, { 'Content-Type': 'text/html' })
132
+ res.end(createErrorHtml('Missing token'))
133
+ server.close()
134
+ reject(new Error('Missing token in callback'))
135
+ return
136
+ }
137
+
138
+ res.writeHead(200, { 'Content-Type': 'text/html' })
139
+ res.end(createSuccessHtml(email || 'unknown'))
140
+
141
+ server.close()
142
+ resolve({ token, mcpUrl, email })
143
+ } else {
144
+ res.writeHead(404)
145
+ res.end('Not found')
146
+ }
147
+ })
148
+
149
+ server.listen(CALLBACK_PORT, '127.0.0.1', () => {
150
+ const authUrl = new URL(`${lanternUrl}/auth/cli`)
151
+ authUrl.searchParams.set('callback_port', CALLBACK_PORT)
152
+ authUrl.searchParams.set('mcp_url', mcpUrlOverride)
153
+ console.log(` Opening browser...`)
154
+ open(authUrl.toString())
155
+ })
156
+
157
+ server.on('error', (err) => {
158
+ if (err.code === 'EADDRINUSE') {
159
+ reject(new Error(`Port ${CALLBACK_PORT} is already in use. Please close any other lantern-connect instances.`))
160
+ } else {
161
+ reject(err)
162
+ }
163
+ })
164
+
165
+ // Timeout after 5 minutes
166
+ setTimeout(() => {
167
+ server.close()
168
+ reject(new Error('Authentication timed out. Please try again.'))
169
+ }, 5 * 60 * 1000)
170
+ })
171
+ }
@@ -0,0 +1,17 @@
1
+ import { execSync } from 'child_process'
2
+
3
+ export function configureClaudeCode(mcpUrl) {
4
+ // First, try to remove existing lantern config (ignore errors if it doesn't exist)
5
+ try {
6
+ execSync('claude mcp remove lantern', { stdio: 'ignore' })
7
+ } catch {
8
+ // Ignore - server might not exist yet
9
+ }
10
+
11
+ // Add the new MCP server
12
+ execSync(`claude mcp add --transport http lantern ${mcpUrl}`, {
13
+ stdio: 'inherit',
14
+ })
15
+
16
+ return 'claude mcp'
17
+ }
@@ -0,0 +1,37 @@
1
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs'
2
+ import { dirname } from 'path'
3
+ import { tools } from '../detect.js'
4
+
5
+ export function configureClaudeDesktop(mcpUrl) {
6
+ const configPath = tools.claudeDesktop.configPath()
7
+
8
+ // Ensure directory exists
9
+ const configDir = dirname(configPath)
10
+ if (!existsSync(configDir)) {
11
+ mkdirSync(configDir, { recursive: true })
12
+ }
13
+
14
+ // Read existing config or create new one
15
+ let config = {}
16
+ if (existsSync(configPath)) {
17
+ try {
18
+ const content = readFileSync(configPath, 'utf-8')
19
+ config = JSON.parse(content)
20
+ } catch {
21
+ // If file exists but is invalid JSON, start fresh
22
+ config = {}
23
+ }
24
+ }
25
+
26
+ // Add/update Lantern MCP server using mcp-remote to bridge HTTP
27
+ config.mcpServers = config.mcpServers || {}
28
+ config.mcpServers.lantern = {
29
+ command: 'npx',
30
+ args: ['mcp-remote', mcpUrl],
31
+ }
32
+
33
+ // Write config
34
+ writeFileSync(configPath, JSON.stringify(config, null, 2))
35
+
36
+ return configPath
37
+ }
@@ -0,0 +1,36 @@
1
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs'
2
+ import { dirname } from 'path'
3
+ import { tools } from '../detect.js'
4
+
5
+ export function configureCursor(mcpUrl) {
6
+ const configPath = tools.cursor.configPath()
7
+
8
+ // Ensure directory exists
9
+ const configDir = dirname(configPath)
10
+ if (!existsSync(configDir)) {
11
+ mkdirSync(configDir, { recursive: true })
12
+ }
13
+
14
+ // Read existing config or create new one
15
+ let config = {}
16
+ if (existsSync(configPath)) {
17
+ try {
18
+ const content = readFileSync(configPath, 'utf-8')
19
+ config = JSON.parse(content)
20
+ } catch {
21
+ // If file exists but is invalid JSON, start fresh
22
+ config = {}
23
+ }
24
+ }
25
+
26
+ // Add/update Lantern MCP server
27
+ config.mcpServers = config.mcpServers || {}
28
+ config.mcpServers.lantern = {
29
+ url: mcpUrl,
30
+ }
31
+
32
+ // Write config
33
+ writeFileSync(configPath, JSON.stringify(config, null, 2))
34
+
35
+ return configPath
36
+ }
@@ -0,0 +1,37 @@
1
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs'
2
+ import { dirname } from 'path'
3
+ import { tools } from '../detect.js'
4
+
5
+ export function configureGeminiCli(mcpUrl) {
6
+ const configPath = tools.geminiCli.configPath()
7
+
8
+ // Ensure directory exists
9
+ const configDir = dirname(configPath)
10
+ if (!existsSync(configDir)) {
11
+ mkdirSync(configDir, { recursive: true })
12
+ }
13
+
14
+ // Read existing config or create new one
15
+ let config = {}
16
+ if (existsSync(configPath)) {
17
+ try {
18
+ const content = readFileSync(configPath, 'utf-8')
19
+ config = JSON.parse(content)
20
+ } catch {
21
+ // If file exists but is invalid JSON, start fresh
22
+ config = {}
23
+ }
24
+ }
25
+
26
+ // Add/update Lantern MCP server
27
+ config.mcpServers = config.mcpServers || {}
28
+ config.mcpServers.lantern = {
29
+ httpUrl: mcpUrl,
30
+ timeout: 30000,
31
+ }
32
+
33
+ // Write config
34
+ writeFileSync(configPath, JSON.stringify(config, null, 2))
35
+
36
+ return configPath
37
+ }
package/src/detect.js ADDED
@@ -0,0 +1,89 @@
1
+ import { existsSync } from 'fs'
2
+ import { homedir, platform } from 'os'
3
+ import { join } from 'path'
4
+ import { execSync } from 'child_process'
5
+
6
+ function expandHome(path) {
7
+ if (path.startsWith('~')) {
8
+ return join(homedir(), path.slice(1))
9
+ }
10
+ return path
11
+ }
12
+
13
+ function commandExists(command) {
14
+ try {
15
+ execSync(`which ${command}`, { stdio: 'ignore' })
16
+ return true
17
+ } catch {
18
+ return false
19
+ }
20
+ }
21
+
22
+ function getClaudeDesktopPath() {
23
+ const os = platform()
24
+ if (os === 'darwin') {
25
+ return expandHome('~/Library/Application Support/Claude')
26
+ } else if (os === 'win32') {
27
+ return join(process.env.APPDATA || '', 'Claude')
28
+ } else {
29
+ // Linux
30
+ return expandHome('~/.config/Claude')
31
+ }
32
+ }
33
+
34
+ function getClaudeDesktopConfigPath() {
35
+ return join(getClaudeDesktopPath(), 'claude_desktop_config.json')
36
+ }
37
+
38
+ function getCursorPath() {
39
+ // Cursor uses ~/.cursor for MCP config on all platforms
40
+ return expandHome('~/.cursor')
41
+ }
42
+
43
+ function getCursorConfigPath() {
44
+ return join(getCursorPath(), 'mcp.json')
45
+ }
46
+
47
+ function getGeminiConfigPath() {
48
+ return expandHome('~/.gemini/settings.json')
49
+ }
50
+
51
+ export const tools = {
52
+ claudeDesktop: {
53
+ name: 'Claude Desktop',
54
+ detect: () => existsSync(getClaudeDesktopPath()),
55
+ configPath: getClaudeDesktopConfigPath,
56
+ },
57
+ claudeCode: {
58
+ name: 'Claude Code',
59
+ detect: () => commandExists('claude'),
60
+ configPath: null, // Uses CLI command instead
61
+ },
62
+ geminiCli: {
63
+ name: 'Gemini CLI',
64
+ detect: () => commandExists('gemini'),
65
+ configPath: getGeminiConfigPath,
66
+ },
67
+ cursor: {
68
+ name: 'Cursor',
69
+ detect: () => existsSync(getCursorPath()),
70
+ configPath: getCursorConfigPath,
71
+ },
72
+ }
73
+
74
+ export function detectTools() {
75
+ const detected = {}
76
+
77
+ for (const [key, tool] of Object.entries(tools)) {
78
+ detected[key] = {
79
+ ...tool,
80
+ found: tool.detect(),
81
+ }
82
+ }
83
+
84
+ return detected
85
+ }
86
+
87
+ export function hasAnyTools(detected) {
88
+ return Object.values(detected).some((tool) => tool.found)
89
+ }
package/src/index.js ADDED
@@ -0,0 +1,147 @@
1
+ import { detectTools, hasAnyTools } from './detect.js'
2
+ import { authenticate } from './auth.js'
3
+ import { configureClaudeDesktop } from './configure/claude-desktop.js'
4
+ import { configureClaudeCode } from './configure/claude-code.js'
5
+ import { configureGeminiCli } from './configure/gemini-cli.js'
6
+ import { configureCursor } from './configure/cursor.js'
7
+
8
+ const configurators = {
9
+ claudeDesktop: configureClaudeDesktop,
10
+ claudeCode: configureClaudeCode,
11
+ geminiCli: configureGeminiCli,
12
+ cursor: configureCursor,
13
+ }
14
+
15
+ function parseArgs() {
16
+ const args = process.argv.slice(2)
17
+ const options = {
18
+ mcpUrl: null,
19
+ lanternUrl: null,
20
+ }
21
+
22
+ for (let i = 0; i < args.length; i++) {
23
+ if (args[i] === '--mcp-url' && args[i + 1]) {
24
+ options.mcpUrl = args[i + 1]
25
+ i++
26
+ } else if (args[i] === '--lantern-url' && args[i + 1]) {
27
+ options.lanternUrl = args[i + 1]
28
+ i++
29
+ } else if (args[i] === '--help' || args[i] === '-h') {
30
+ console.log(`
31
+ Usage: lantern-connect [options]
32
+
33
+ Options:
34
+ --mcp-url <url> MCP server URL (default: https://api.onlantern.com/mcp)
35
+ --lantern-url <url> Lantern web URL (default: https://onlantern.com)
36
+ --help, -h Show this help message
37
+ `)
38
+ process.exit(0)
39
+ }
40
+ }
41
+
42
+ return options
43
+ }
44
+
45
+ export async function main() {
46
+ const options = parseArgs()
47
+ console.log('')
48
+ console.log(' šŸ”„ Lantern Connect')
49
+ console.log(' ─────────────────')
50
+ console.log('')
51
+
52
+ // Step 1: Detect installed tools
53
+ console.log(' šŸ” Detecting AI tools...')
54
+ console.log('')
55
+
56
+ const detected = detectTools()
57
+
58
+ for (const [key, tool] of Object.entries(detected)) {
59
+ const icon = tool.found ? 'āœ“' : 'āœ—'
60
+ const color = tool.found ? '\x1b[32m' : '\x1b[90m'
61
+ const reset = '\x1b[0m'
62
+ console.log(` ${color}${icon} ${tool.name}${reset}`)
63
+ }
64
+
65
+ console.log('')
66
+
67
+ if (!hasAnyTools(detected)) {
68
+ console.log(' āš ļø No supported AI tools found.')
69
+ console.log('')
70
+ console.log(' Supported tools:')
71
+ console.log(' • Claude Desktop')
72
+ console.log(' • Claude Code (claude CLI)')
73
+ console.log(' • Gemini CLI')
74
+ console.log(' • Cursor')
75
+ console.log('')
76
+ process.exit(1)
77
+ }
78
+
79
+ // Step 2: Authenticate
80
+ console.log(' šŸ” Authenticating with Lantern...')
81
+
82
+ let authResult
83
+ try {
84
+ authResult = await authenticate({
85
+ lanternUrl: options.lanternUrl,
86
+ mcpUrl: options.mcpUrl,
87
+ })
88
+ } catch (err) {
89
+ console.log('')
90
+ console.log(` āŒ Authentication failed: ${err.message}`)
91
+ console.log('')
92
+ process.exit(1)
93
+ }
94
+
95
+ console.log(` āœ“ Logged in as ${authResult.email}`)
96
+ console.log('')
97
+
98
+ // Step 3: Configure each detected tool
99
+ console.log(' šŸ“ Configuring...')
100
+ console.log('')
101
+
102
+ const configured = []
103
+ const failed = []
104
+
105
+ for (const [key, tool] of Object.entries(detected)) {
106
+ if (!tool.found) continue
107
+
108
+ const configurator = configurators[key]
109
+ if (!configurator) continue
110
+
111
+ try {
112
+ const result = configurator(authResult.mcpUrl)
113
+ configured.push({ name: tool.name, path: result })
114
+ console.log(` āœ“ ${tool.name}`)
115
+ } catch (err) {
116
+ failed.push({ name: tool.name, error: err.message })
117
+ console.log(` āœ— ${tool.name}: ${err.message}`)
118
+ }
119
+ }
120
+
121
+ console.log('')
122
+
123
+ // Step 4: Summary
124
+ if (configured.length > 0) {
125
+ console.log(' šŸŽ‰ Done!')
126
+ console.log('')
127
+ console.log(' Next steps:')
128
+ console.log(' 1. Fully quit and reopen your AI tools (Cmd+Q on macOS)')
129
+
130
+ // Check if Claude Desktop was configured
131
+ if (detected.claudeDesktop?.found) {
132
+ console.log(' 2. In Claude Desktop: Settings → Connectors → click "lantern" to authenticate')
133
+ console.log(' 3. Say "save to lantern" to export a conversation')
134
+ } else {
135
+ console.log(' 2. Say "save to lantern" to export a conversation')
136
+ }
137
+ console.log('')
138
+ }
139
+
140
+ if (failed.length > 0) {
141
+ console.log(' āš ļø Some tools could not be configured:')
142
+ for (const f of failed) {
143
+ console.log(` • ${f.name}: ${f.error}`)
144
+ }
145
+ console.log('')
146
+ }
147
+ }