@phenx-inc/ctlsurf 0.1.0 → 0.1.2

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.
@@ -4,7 +4,8 @@
4
4
  * ctlsurf launcher
5
5
  *
6
6
  * Subcommands:
7
- * ctlsurf update pull latest and rebuild
7
+ * ctlsurf setup configure MCP for all detected coding agents
8
+ * ctlsurf update — update to latest version
8
9
  * ctlsurf doctor — check all dependencies
9
10
  * ctlsurf version — print version
10
11
  *
@@ -16,11 +17,95 @@
16
17
  const { execFileSync, execSync } = require('child_process')
17
18
  const path = require('path')
18
19
  const fs = require('fs')
20
+ const os = require('os')
19
21
 
20
22
  const ROOT = path.resolve(__dirname, '..')
21
23
  const args = process.argv.slice(2)
22
24
  const subcommand = args[0]
23
25
 
26
+ // ─── Colors ───────────────────────────────────────
27
+
28
+ const G = '\x1b[32m'
29
+ const Y = '\x1b[33m'
30
+ const B = '\x1b[34m'
31
+ const D = '\x1b[90m'
32
+ const R = '\x1b[0m'
33
+
34
+ // ─── MCP check ────────────────────────────────────
35
+
36
+ function isMcpConfigured() {
37
+ // Check env var
38
+ if (process.env.CTLSURF_API_KEY) return true
39
+
40
+ // Check Claude Code settings
41
+ try {
42
+ const claudeSettings = path.join(os.homedir(), '.claude', 'settings.json')
43
+ if (fs.existsSync(claudeSettings)) {
44
+ const data = JSON.parse(fs.readFileSync(claudeSettings, 'utf-8'))
45
+ if (data.mcpServers && data.mcpServers.ctlsurf) return true
46
+ }
47
+ } catch {}
48
+
49
+ // Check Codex config
50
+ try {
51
+ const codexConfig = path.join(os.homedir(), '.codex', 'config.toml')
52
+ if (fs.existsSync(codexConfig)) {
53
+ const data = fs.readFileSync(codexConfig, 'utf-8')
54
+ if (data.includes('[mcp_servers.ctlsurf]')) return true
55
+ }
56
+ } catch {}
57
+
58
+ // Check Cursor config
59
+ try {
60
+ const cursorMcp = path.join(os.homedir(), '.cursor', 'mcp.json')
61
+ if (fs.existsSync(cursorMcp)) {
62
+ const data = JSON.parse(fs.readFileSync(cursorMcp, 'utf-8'))
63
+ if (data.mcpServers && data.mcpServers.ctlsurf) return true
64
+ }
65
+ } catch {}
66
+
67
+ return false
68
+ }
69
+
70
+ function runSetup() {
71
+ const setupScript = path.join(ROOT, 'scripts', 'setup-mcp.sh')
72
+ if (!fs.existsSync(setupScript)) {
73
+ console.error(`${Y}!${R} Setup script not found: ${setupScript}`)
74
+ console.error(` Run the web installer instead: curl -fsSL https://app.ctlsurf.com/install.sh | bash`)
75
+ process.exit(1)
76
+ }
77
+ try {
78
+ execFileSync('bash', [setupScript, ...args.slice(1)], { stdio: 'inherit' })
79
+ } catch (err) {
80
+ process.exit(err.status || 1)
81
+ }
82
+ }
83
+
84
+ function promptSetup() {
85
+ console.log(`\n${B} ctlsurf${R}\n`)
86
+ console.log(` MCP is not configured for any coding agent.`)
87
+ console.log(` Run setup to connect ctlsurf to Claude Code, Codex, Copilot, and Cursor.\n`)
88
+ console.log(` ${B}ctlsurf setup${R} ${D}# interactive setup${R}`)
89
+ console.log(` ${D}ctlsurf --terminal${R} ${D}# skip setup, launch anyway${R}\n`)
90
+
91
+ // Interactive prompt
92
+ if (process.stdin.isTTY) {
93
+ const readline = require('readline')
94
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
95
+ rl.question(` Run setup now? ${D}(Y/n)${R} `, (answer) => {
96
+ rl.close()
97
+ if (!answer || answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes') {
98
+ console.log('')
99
+ runSetup()
100
+ } else {
101
+ console.log('')
102
+ }
103
+ })
104
+ return true // handled
105
+ }
106
+ return false
107
+ }
108
+
24
109
  // ─── Subcommands ──────────────────────────────────
25
110
 
26
111
  if (subcommand === 'version' || args.includes('--version') || args.includes('-v')) {
@@ -33,20 +118,18 @@ if (subcommand === 'version' || args.includes('--version') || args.includes('-v'
33
118
  process.exit(0)
34
119
  }
35
120
 
36
- if (subcommand === 'update') {
37
- const G = '\x1b[32m'
38
- const Y = '\x1b[33m'
39
- const B = '\x1b[34m'
40
- const D = '\x1b[90m'
41
- const R = '\x1b[0m'
121
+ if (subcommand === 'setup') {
122
+ runSetup()
123
+ process.exit(0)
124
+ }
42
125
 
126
+ if (subcommand === 'update') {
43
127
  console.log(`\n${B} ctlsurf update${R}\n`)
44
128
 
45
129
  try {
46
130
  console.log(`${D}Updating via npm...${R}`)
47
131
  execSync('npm update -g @phenx-inc/ctlsurf', { stdio: 'inherit' })
48
132
 
49
- // Re-read version after update
50
133
  let ver = '?'
51
134
  try { ver = require(path.join(ROOT, 'package.json')).version } catch {}
52
135
  console.log(`\n${G}✓${R} ctlsurf v${ver}\n`)
@@ -58,12 +141,6 @@ if (subcommand === 'update') {
58
141
  }
59
142
 
60
143
  if (subcommand === 'doctor') {
61
- const G = '\x1b[32m'
62
- const Y = '\x1b[33m'
63
- const R = '\x1b[0m'
64
- const D = '\x1b[90m'
65
- const B = '\x1b[34m'
66
-
67
144
  console.log(`\n${B} ctlsurf doctor${R}\n`)
68
145
 
69
146
  function check(name, cmd, required, fix) {
@@ -88,7 +165,7 @@ if (subcommand === 'doctor') {
88
165
  check('npm', 'npm --version', true)
89
166
  check('git', 'git --version', true)
90
167
 
91
- // node-pty native addon
168
+ // node-pty
92
169
  try {
93
170
  require(path.join(ROOT, 'node_modules/node-pty'))
94
171
  console.log(` ${G}✓${R} node-pty ${D}(native addon loaded)${R}`)
@@ -105,6 +182,14 @@ if (subcommand === 'doctor') {
105
182
 
106
183
  console.log('')
107
184
 
185
+ // MCP config
186
+ if (isMcpConfigured()) {
187
+ console.log(` ${G}✓${R} MCP configured ${D}(ctlsurf server connected)${R}`)
188
+ } else {
189
+ console.log(` ${Y}✗${R} MCP not configured`)
190
+ console.log(` ${D}Run: ctlsurf setup${R}`)
191
+ }
192
+
108
193
  // Desktop
109
194
  try {
110
195
  require.resolve('electron')
@@ -125,6 +210,14 @@ if (subcommand === 'doctor') {
125
210
  process.exit(0)
126
211
  }
127
212
 
213
+ // ─── Auto-detect MCP on first run ─────────────────
214
+
215
+ if (!subcommand || subcommand.startsWith('--')) {
216
+ if (!isMcpConfigured() && !args.includes('--skip-setup')) {
217
+ if (promptSetup()) return // async prompt, exits after
218
+ }
219
+ }
220
+
128
221
  // ─── Mode detection ───────────────────────────────
129
222
 
130
223
  function detectMode(argv) {
@@ -145,13 +238,57 @@ if (mode === 'desktop') {
145
238
  try {
146
239
  electronPath = require('electron')
147
240
  } catch {
148
- console.error('[ctlsurf] Electron not found, falling back to terminal mode')
241
+ // Electron not installed prompt to install or fall back
242
+ if (process.stdin.isTTY && !args.includes('--terminal')) {
243
+ console.log(`\n${B} ctlsurf${R}\n`)
244
+ console.log(` Electron is not installed. Desktop mode requires Electron.\n`)
245
+ console.log(` ${D}1)${R} Install Electron and launch desktop mode`)
246
+ console.log(` ${D}2)${R} Launch in terminal mode instead`)
247
+ console.log(` ${D}3)${R} Exit\n`)
248
+
249
+ const readline = require('readline')
250
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
251
+ rl.question(` Choice ${D}(1/2/3)${R}: `, (answer) => {
252
+ rl.close()
253
+ answer = answer.trim()
254
+ if (answer === '1') {
255
+ console.log(`\n${D}Installing Electron (this may take a minute)...${R}\n`)
256
+ try {
257
+ execSync('npm install electron --no-save', { cwd: ROOT, stdio: 'inherit' })
258
+ console.log(`\n${G}✓${R} Electron installed. Launching desktop mode...\n`)
259
+ try {
260
+ const ep = require('electron')
261
+ execFileSync(String(ep), [ROOT, ...args], {
262
+ stdio: 'inherit',
263
+ env: { ...process.env, CTLSURF_WORKER_CWD: process.env.CTLSURF_WORKER_CWD || process.cwd() }
264
+ })
265
+ } catch (err) {
266
+ process.exit(err.status || 0)
267
+ }
268
+ } catch (err) {
269
+ console.error(`\n${Y}!${R} Electron install failed: ${err.message}`)
270
+ console.error(` Try manually: cd ${ROOT} && npm install electron\n`)
271
+ process.exit(1)
272
+ }
273
+ } else if (answer === '2') {
274
+ console.log('')
275
+ runTerminal()
276
+ } else {
277
+ process.exit(0)
278
+ }
279
+ })
280
+ return
281
+ }
282
+ // Non-interactive: fall back silently
149
283
  runTerminal()
150
284
  process.exit(0)
151
285
  }
152
286
 
153
287
  try {
154
- execFileSync(String(electronPath), [ROOT, ...args], { stdio: 'inherit' })
288
+ execFileSync(String(electronPath), [ROOT, ...args], {
289
+ stdio: 'inherit',
290
+ env: { ...process.env, CTLSURF_WORKER_CWD: process.env.CTLSURF_WORKER_CWD || process.cwd() }
291
+ })
155
292
  } catch (err) {
156
293
  process.exit(err.status || 0)
157
294
  }
@@ -166,7 +303,10 @@ function runTerminal() {
166
303
  process.exit(1)
167
304
  }
168
305
  try {
169
- execFileSync(process.execPath, [terminalPath, ...args], { stdio: 'inherit' })
306
+ execFileSync(process.execPath, [terminalPath, ...args], {
307
+ stdio: 'inherit',
308
+ env: { ...process.env, CTLSURF_WORKER_CWD: process.env.CTLSURF_WORKER_CWD || process.cwd() }
309
+ })
170
310
  } catch (err) {
171
311
  process.exit(err.status || 0)
172
312
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phenx-inc/ctlsurf",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Agent-agnostic terminal and desktop app for ctlsurf — run Claude Code, Codex, or any coding agent with live session logging and remote control",
5
5
  "main": "out/main/index.js",
6
6
  "bin": {
@@ -11,6 +11,7 @@
11
11
  "bin/",
12
12
  "out/",
13
13
  "resources/",
14
+ "scripts/",
14
15
  "src/",
15
16
  "package.json",
16
17
  "electron-vite.config.ts",
@@ -0,0 +1,90 @@
1
+ #!/bin/sh
2
+ set -e
3
+
4
+ # ─── Bundle ctlsurf for distribution ──────────────
5
+ #
6
+ # Creates a platform-specific tarball with everything needed to run.
7
+ # No build tools required on the target machine.
8
+ #
9
+ # Output: dist/ctlsurf-<version>-<os>-<arch>.tar.gz
10
+
11
+ ROOT="$(cd "$(dirname "$0")/.." && pwd)"
12
+ cd "$ROOT"
13
+
14
+ VERSION=$(node -e "console.log(require('./package.json').version)")
15
+ OS=$(uname -s | tr '[:upper:]' '[:lower:]')
16
+ ARCH=$(uname -m)
17
+
18
+ # Normalize arch names
19
+ case "$ARCH" in
20
+ x86_64) ARCH="x64" ;;
21
+ aarch64) ARCH="arm64" ;;
22
+ arm64) ARCH="arm64" ;;
23
+ esac
24
+
25
+ BUNDLE_NAME="ctlsurf-${VERSION}-${OS}-${ARCH}"
26
+ DIST_DIR="$ROOT/dist"
27
+ STAGE_DIR="$DIST_DIR/$BUNDLE_NAME"
28
+
29
+ echo "Building ctlsurf v${VERSION} for ${OS}-${ARCH}..."
30
+
31
+ # ─── Build ─────────────────────────────────────────
32
+
33
+ echo "Building headless..."
34
+ npm run build:headless
35
+
36
+ echo "Building desktop..."
37
+ npx electron-vite build 2>/dev/null || echo "(desktop build skipped — no electron)"
38
+
39
+ # ─── Stage ─────────────────────────────────────────
40
+
41
+ rm -rf "$STAGE_DIR"
42
+ mkdir -p "$STAGE_DIR"
43
+
44
+ # Bin
45
+ cp -r bin "$STAGE_DIR/bin"
46
+ chmod +x "$STAGE_DIR/bin/ctlsurf-worker.js"
47
+
48
+ # Built output
49
+ cp -r out "$STAGE_DIR/out"
50
+
51
+ # Package metadata
52
+ cp package.json "$STAGE_DIR/package.json"
53
+
54
+ # Resources (icons)
55
+ if [ -d resources ]; then
56
+ cp -r resources "$STAGE_DIR/resources"
57
+ fi
58
+
59
+ # node_modules — only runtime deps, with native addons pre-compiled
60
+ mkdir -p "$STAGE_DIR/node_modules"
61
+
62
+ # node-pty (native addon — platform-specific)
63
+ if [ -d node_modules/node-pty ]; then
64
+ cp -r node_modules/node-pty "$STAGE_DIR/node_modules/node-pty"
65
+ fi
66
+
67
+ # nan (node-pty dependency)
68
+ if [ -d node_modules/nan ]; then
69
+ cp -r node_modules/nan "$STAGE_DIR/node_modules/nan"
70
+ fi
71
+
72
+ # node-addon-api (node-pty dependency)
73
+ if [ -d node_modules/node-addon-api ]; then
74
+ cp -r node_modules/node-addon-api "$STAGE_DIR/node_modules/node-addon-api"
75
+ fi
76
+
77
+ # ─── Tarball ───────────────────────────────────────
78
+
79
+ echo "Creating tarball..."
80
+ cd "$DIST_DIR"
81
+ tar -czf "${BUNDLE_NAME}.tar.gz" "$BUNDLE_NAME"
82
+ rm -rf "$STAGE_DIR"
83
+
84
+ SIZE=$(du -h "${BUNDLE_NAME}.tar.gz" | awk '{print $1}')
85
+ echo ""
86
+ echo "✓ dist/${BUNDLE_NAME}.tar.gz (${SIZE})"
87
+ echo ""
88
+ echo "Install on target machine:"
89
+ echo " tar -xzf ${BUNDLE_NAME}.tar.gz -C ~/.ctlsurf --strip-components=1"
90
+ echo " ln -sf ~/.ctlsurf/bin/ctlsurf-worker.js /usr/local/bin/ctlsurf"
@@ -0,0 +1,764 @@
1
+ #!/bin/bash
2
+ #
3
+ # ctlsurf MCP Setup
4
+ # Usage: curl -fsSL https://app.ctlsurf.com/install.sh | bash
5
+ #
6
+ # Options (pass via: curl ... | bash -s -- --flag):
7
+ # --force-codex Force Codex configuration even if not detected
8
+ # --force-claude Force Claude configuration even if not detected
9
+ # --force-copilot Force GitHub Copilot configuration even if not detected
10
+ # --force-cursor Force Cursor configuration even if not detected
11
+ # --force Force all configurations
12
+ #
13
+ # Detects and configures:
14
+ # - Claude Code CLI
15
+ # - Claude Code VSCode Extension
16
+ # - OpenAI Codex CLI
17
+ # - OpenAI Codex VSCode Extension
18
+ # - GitHub Copilot VSCode Extension
19
+ # - Cursor Editor
20
+ #
21
+
22
+ set -e
23
+
24
+ # Parse command line arguments
25
+ FORCE_CODEX=false
26
+ FORCE_CLAUDE=false
27
+ FORCE_COPILOT=false
28
+ FORCE_CURSOR=false
29
+ while [[ $# -gt 0 ]]; do
30
+ case $1 in
31
+ --force-codex) FORCE_CODEX=true; shift ;;
32
+ --force-claude) FORCE_CLAUDE=true; shift ;;
33
+ --force-copilot) FORCE_COPILOT=true; shift ;;
34
+ --force-cursor) FORCE_CURSOR=true; shift ;;
35
+ --force) FORCE_CODEX=true; FORCE_CLAUDE=true; FORCE_COPILOT=true; FORCE_CURSOR=true; shift ;;
36
+ *) shift ;;
37
+ esac
38
+ done
39
+
40
+ echo "========================================"
41
+ echo " ctlsurf MCP Setup"
42
+ echo "========================================"
43
+ echo ""
44
+
45
+ RED='\033[0;31m'
46
+ GREEN='\033[0;32m'
47
+ YELLOW='\033[0;33m'
48
+ BLUE='\033[0;34m'
49
+ NC='\033[0m'
50
+
51
+ FOUND_CLAUDE_CLI=false
52
+ FOUND_CLAUDE_VSCODE=false
53
+ FOUND_CODEX_CLI=false
54
+ FOUND_CODEX_VSCODE=false
55
+ FOUND_COPILOT=false
56
+ FOUND_CURSOR=false
57
+
58
+ # Determine VSCode settings path based on OS
59
+ get_vscode_settings_path() {
60
+ if [[ "$OSTYPE" == "darwin"* ]]; then
61
+ echo "$HOME/Library/Application Support/Code/User/settings.json"
62
+ elif [[ "$OSTYPE" == "linux-gnu"* ]]; then
63
+ echo "$HOME/.config/Code/User/settings.json"
64
+ elif [[ "$OSTYPE" == "msys" ]] || [[ "$OSTYPE" == "cygwin" ]] || [[ "$OSTYPE" == "win32" ]]; then
65
+ echo "$APPDATA/Code/User/settings.json"
66
+ else
67
+ echo ""
68
+ fi
69
+ }
70
+
71
+ # Determine VSCode mcp.json path based on OS (for Copilot)
72
+ get_vscode_mcp_path() {
73
+ if [[ "$OSTYPE" == "darwin"* ]]; then
74
+ echo "$HOME/Library/Application Support/Code/User/mcp.json"
75
+ elif [[ "$OSTYPE" == "linux-gnu"* ]]; then
76
+ echo "$HOME/.config/Code/User/mcp.json"
77
+ elif [[ "$OSTYPE" == "msys" ]] || [[ "$OSTYPE" == "cygwin" ]] || [[ "$OSTYPE" == "win32" ]]; then
78
+ echo "$APPDATA/Code/User/mcp.json"
79
+ else
80
+ echo ""
81
+ fi
82
+ }
83
+
84
+ # Check if a VSCode extension is installed
85
+ check_vscode_extension() {
86
+ local ext_id="$1"
87
+
88
+ # Method 1: Try using code CLI
89
+ if command -v code &> /dev/null; then
90
+ if code --list-extensions 2>/dev/null | grep -qi "$ext_id"; then
91
+ return 0
92
+ fi
93
+ fi
94
+
95
+ # Method 2: Check extensions folder directly (fallback when code not in PATH)
96
+ local ext_dir="$HOME/.vscode/extensions"
97
+ if [ -d "$ext_dir" ]; then
98
+ if ls "$ext_dir" 2>/dev/null | grep -qi "$ext_id"; then
99
+ return 0
100
+ fi
101
+ fi
102
+
103
+ return 1
104
+ }
105
+
106
+ echo -e "${BLUE}Detecting installed tools...${NC}"
107
+ echo ""
108
+
109
+ # Check for Claude CLI
110
+ if [ "$FORCE_CLAUDE" = true ]; then
111
+ echo -e "${GREEN}✓ Claude Code CLI (forced)${NC}"
112
+ FOUND_CLAUDE_CLI=true
113
+ elif command -v claude &> /dev/null; then
114
+ echo -e "${GREEN}✓ Found Claude Code CLI${NC}"
115
+ FOUND_CLAUDE_CLI=true
116
+ else
117
+ echo -e "${YELLOW}○ Claude Code CLI not found${NC}"
118
+ fi
119
+
120
+ # Check for Claude Code VSCode Extension
121
+ if check_vscode_extension "anthropic.claude-code"; then
122
+ echo -e "${GREEN}✓ Found Claude Code VSCode Extension${NC}"
123
+ FOUND_CLAUDE_VSCODE=true
124
+ else
125
+ echo -e "${YELLOW}○ Claude Code VSCode Extension not found${NC}"
126
+ fi
127
+
128
+ # Check for Codex CLI (also check ~/.codex dir for installations not in PATH)
129
+ if [ "$FORCE_CODEX" = true ]; then
130
+ echo -e "${GREEN}✓ OpenAI Codex CLI (forced)${NC}"
131
+ FOUND_CODEX_CLI=true
132
+ elif command -v codex &> /dev/null; then
133
+ echo -e "${GREEN}✓ Found OpenAI Codex CLI${NC}"
134
+ FOUND_CODEX_CLI=true
135
+ elif [ -d "$HOME/.codex" ]; then
136
+ echo -e "${GREEN}✓ Found OpenAI Codex config directory${NC}"
137
+ FOUND_CODEX_CLI=true
138
+ else
139
+ echo -e "${YELLOW}○ OpenAI Codex CLI not found${NC}"
140
+ fi
141
+
142
+ # Check for Codex/ChatGPT VSCode Extension (openai.codex or openai.chatgpt)
143
+ if check_vscode_extension "openai.codex" || check_vscode_extension "openai.chatgpt"; then
144
+ echo -e "${GREEN}✓ Found OpenAI Codex VSCode Extension${NC}"
145
+ FOUND_CODEX_VSCODE=true
146
+ else
147
+ echo -e "${YELLOW}○ OpenAI Codex VSCode Extension not found${NC}"
148
+ fi
149
+
150
+ # Check for GitHub Copilot VSCode Extension
151
+ if [ "$FORCE_COPILOT" = true ]; then
152
+ echo -e "${GREEN}✓ GitHub Copilot (forced)${NC}"
153
+ FOUND_COPILOT=true
154
+ elif check_vscode_extension "github.copilot"; then
155
+ echo -e "${GREEN}✓ Found GitHub Copilot VSCode Extension${NC}"
156
+ FOUND_COPILOT=true
157
+ else
158
+ echo -e "${YELLOW}○ GitHub Copilot VSCode Extension not found${NC}"
159
+ fi
160
+
161
+ # Check for Cursor Editor (check command or config directory)
162
+ if [ "$FORCE_CURSOR" = true ]; then
163
+ echo -e "${GREEN}✓ Cursor Editor (forced)${NC}"
164
+ FOUND_CURSOR=true
165
+ elif command -v cursor &> /dev/null; then
166
+ echo -e "${GREEN}✓ Found Cursor Editor${NC}"
167
+ FOUND_CURSOR=true
168
+ elif [ -d "$HOME/.cursor" ]; then
169
+ echo -e "${GREEN}✓ Found Cursor config directory${NC}"
170
+ FOUND_CURSOR=true
171
+ elif [ -d "/Applications/Cursor.app" ]; then
172
+ echo -e "${GREEN}✓ Found Cursor.app${NC}"
173
+ FOUND_CURSOR=true
174
+ else
175
+ echo -e "${YELLOW}○ Cursor Editor not found${NC}"
176
+ fi
177
+
178
+ echo ""
179
+
180
+ # Exit if nothing found
181
+ if [ "$FOUND_CLAUDE_CLI" = false ] && [ "$FOUND_CLAUDE_VSCODE" = false ] && \
182
+ [ "$FOUND_CODEX_CLI" = false ] && [ "$FOUND_CODEX_VSCODE" = false ] && \
183
+ [ "$FOUND_COPILOT" = false ] && [ "$FOUND_CURSOR" = false ]; then
184
+ echo -e "${RED}ERROR: No supported AI tools found.${NC}"
185
+ echo ""
186
+ echo "Install one of the following:"
187
+ echo " - Claude Code CLI: https://claude.ai/download"
188
+ echo " - Claude Code VSCode: Search 'Claude Code' in VSCode Extensions"
189
+ echo " - Codex CLI: npm install -g @openai/codex"
190
+ echo " - Codex VSCode: Search 'OpenAI Codex' in VSCode Extensions"
191
+ echo " - GitHub Copilot: Search 'GitHub Copilot' in VSCode Extensions"
192
+ echo " - Cursor: https://cursor.com"
193
+ echo ""
194
+ echo "Or force configuration if detection failed:"
195
+ echo " curl -fsSL https://app.ctlsurf.com/install.sh | bash -s -- --force-codex"
196
+ echo " curl -fsSL https://app.ctlsurf.com/install.sh | bash -s -- --force-claude"
197
+ echo " curl -fsSL https://app.ctlsurf.com/install.sh | bash -s -- --force-copilot"
198
+ echo " curl -fsSL https://app.ctlsurf.com/install.sh | bash -s -- --force-cursor"
199
+ exit 1
200
+ fi
201
+
202
+ # Check for existing API key in environment or shell profile
203
+ EXISTING_KEY=""
204
+ EXISTING_URL=""
205
+
206
+ # Check env var
207
+ if [ -n "$CTLSURF_API_KEY" ]; then
208
+ EXISTING_KEY="$CTLSURF_API_KEY"
209
+ fi
210
+
211
+ # Check shell profile
212
+ if [ -z "$EXISTING_KEY" ]; then
213
+ for rc in "$HOME/.zshrc" "$HOME/.bashrc" "$HOME/.bash_profile"; do
214
+ if [ -f "$rc" ]; then
215
+ KEY_FROM_RC=$(grep '^export CTLSURF_API_KEY=' "$rc" 2>/dev/null | tail -1 | sed 's/^export CTLSURF_API_KEY="//' | sed 's/"$//')
216
+ if [ -n "$KEY_FROM_RC" ]; then
217
+ EXISTING_KEY="$KEY_FROM_RC"
218
+ break
219
+ fi
220
+ fi
221
+ done
222
+ fi
223
+
224
+ # Check CTLSURF_URL from env or profile
225
+ if [ -n "$CTLSURF_URL" ]; then
226
+ EXISTING_URL="$CTLSURF_URL"
227
+ else
228
+ for rc in "$HOME/.zshrc" "$HOME/.bashrc" "$HOME/.bash_profile"; do
229
+ if [ -f "$rc" ]; then
230
+ URL_FROM_RC=$(grep '^export CTLSURF_URL=' "$rc" 2>/dev/null | tail -1 | sed 's/^export CTLSURF_URL="//' | sed 's/"$//')
231
+ if [ -n "$URL_FROM_RC" ]; then
232
+ EXISTING_URL="$URL_FROM_RC"
233
+ break
234
+ fi
235
+ fi
236
+ done
237
+ fi
238
+
239
+ # Use existing credentials or prompt
240
+ if [ -n "$EXISTING_KEY" ]; then
241
+ MASKED_KEY="${EXISTING_KEY:0:8}...${EXISTING_KEY: -4}"
242
+ echo -e "${GREEN}✓ Found existing API key: ${MASKED_KEY}${NC}"
243
+ echo -n "Use this key? (Y/n): "
244
+ read USE_EXISTING < /dev/tty
245
+ if [ -z "$USE_EXISTING" ] || [ "$USE_EXISTING" = "y" ] || [ "$USE_EXISTING" = "Y" ]; then
246
+ API_KEY="$EXISTING_KEY"
247
+ else
248
+ echo ""
249
+ echo "Get your API key from: https://app.ctlsurf.com/settings (API Keys tab)"
250
+ echo -n "Enter your ctlsurf API key: "
251
+ read API_KEY < /dev/tty
252
+ fi
253
+ else
254
+ echo "Get your API key from: https://app.ctlsurf.com/settings (API Keys tab)"
255
+ echo -n "Enter your ctlsurf API key: "
256
+ read API_KEY < /dev/tty
257
+ fi
258
+
259
+ if [ -z "$API_KEY" ]; then
260
+ echo -e "${RED}ERROR: API key is required${NC}"
261
+ exit 1
262
+ fi
263
+
264
+ echo ""
265
+
266
+ if [ -n "$EXISTING_URL" ]; then
267
+ # Strip /api/mcp suffix for display
268
+ DISPLAY_URL=$(echo "$EXISTING_URL" | sed 's|/api/mcp$||' | sed 's|/api$||')
269
+ echo -e "${GREEN}✓ Found existing server URL: ${DISPLAY_URL}${NC}"
270
+ echo -n "Use this URL? (Y/n): "
271
+ read USE_EXISTING_URL < /dev/tty
272
+ if [ -z "$USE_EXISTING_URL" ] || [ "$USE_EXISTING_URL" = "y" ] || [ "$USE_EXISTING_URL" = "Y" ]; then
273
+ SERVER_URL="$DISPLAY_URL"
274
+ else
275
+ echo -n "Server URL [https://app.ctlsurf.com]: "
276
+ read SERVER_URL < /dev/tty
277
+ SERVER_URL=${SERVER_URL:-https://app.ctlsurf.com}
278
+ fi
279
+ else
280
+ echo -n "Server URL [https://app.ctlsurf.com]: "
281
+ read SERVER_URL < /dev/tty
282
+ SERVER_URL=${SERVER_URL:-https://app.ctlsurf.com}
283
+ fi
284
+ # Normalize URL
285
+ SERVER_URL="${SERVER_URL%/}"
286
+ SERVER_URL="${SERVER_URL%/api}"
287
+ SERVER_URL="${SERVER_URL%/api/mcp}"
288
+ MCP_URL="${SERVER_URL}/api/mcp"
289
+
290
+ echo ""
291
+ echo "Configuring ctlsurf..."
292
+ echo ""
293
+
294
+ # Persist API key to shell profile
295
+ SHELL_PROFILE=""
296
+ if [ -f "$HOME/.zshrc" ]; then
297
+ SHELL_PROFILE="$HOME/.zshrc"
298
+ elif [ -f "$HOME/.bashrc" ]; then
299
+ SHELL_PROFILE="$HOME/.bashrc"
300
+ elif [ -f "$HOME/.bash_profile" ]; then
301
+ SHELL_PROFILE="$HOME/.bash_profile"
302
+ fi
303
+
304
+ if [ -n "$SHELL_PROFILE" ]; then
305
+ # Remove existing CTLSURF exports
306
+ if [[ "$OSTYPE" == "darwin"* ]]; then
307
+ sed -i '' '/^export CTLSURF_/d' "$SHELL_PROFILE" 2>/dev/null || true
308
+ else
309
+ sed -i '/^export CTLSURF_/d' "$SHELL_PROFILE" 2>/dev/null || true
310
+ fi
311
+
312
+ # Add new exports
313
+ echo "" >> "$SHELL_PROFILE"
314
+ echo "# ctlsurf MCP configuration" >> "$SHELL_PROFILE"
315
+ echo "export CTLSURF_API_KEY=\"$API_KEY\"" >> "$SHELL_PROFILE"
316
+ echo "export CTLSURF_URL=\"$MCP_URL\"" >> "$SHELL_PROFILE"
317
+
318
+ echo -e "${GREEN}✓ Added API key to $SHELL_PROFILE${NC}"
319
+ echo ""
320
+ fi
321
+
322
+ CONFIGURED_COUNT=0
323
+ CONFIGURED_TOOLS=""
324
+
325
+ # ============================================================
326
+ # Configure Claude Code CLI
327
+ # ============================================================
328
+ if [ "$FOUND_CLAUDE_CLI" = true ]; then
329
+ echo "Setting up Claude Code CLI..."
330
+
331
+ # Check for stale .mcp.json files that could shadow the user-level config
332
+ for MCP_JSON_PATH in "$HOME/.mcp.json" "./.mcp.json"; do
333
+ if [ -f "$MCP_JSON_PATH" ] && grep -q "ctlsurf" "$MCP_JSON_PATH" 2>/dev/null; then
334
+ # Check if it has an old stdio-based config (command-based, not HTTP)
335
+ if grep -q '"command"' "$MCP_JSON_PATH" 2>/dev/null && ! grep -q '"type".*"http"' "$MCP_JSON_PATH" 2>/dev/null; then
336
+ echo -e "${YELLOW} ⚠ Found stale ctlsurf config in $MCP_JSON_PATH (old stdio transport)${NC}"
337
+ echo -e "${YELLOW} This will shadow the HTTP config and cause connection failures.${NC}"
338
+ if command -v jq &> /dev/null; then
339
+ # Remove ctlsurf entry from .mcp.json using jq
340
+ TEMP_MCP_JSON=$(mktemp)
341
+ jq 'del(.mcpServers.ctlsurf)' "$MCP_JSON_PATH" > "$TEMP_MCP_JSON" 2>/dev/null
342
+ # If .mcp.json is now empty (no other servers), remove the file
343
+ if jq -e '.mcpServers | length == 0' "$TEMP_MCP_JSON" &>/dev/null; then
344
+ rm "$MCP_JSON_PATH"
345
+ rm "$TEMP_MCP_JSON"
346
+ echo -e "${GREEN} Removed empty $MCP_JSON_PATH${NC}"
347
+ else
348
+ mv "$TEMP_MCP_JSON" "$MCP_JSON_PATH"
349
+ echo -e "${GREEN} Removed stale ctlsurf entry from $MCP_JSON_PATH${NC}"
350
+ fi
351
+ else
352
+ echo -e "${YELLOW} Please remove ctlsurf from $MCP_JSON_PATH manually${NC}"
353
+ fi
354
+ fi
355
+ fi
356
+ done
357
+
358
+ # Remove existing config
359
+ claude mcp remove ctlsurf -s user 2>/dev/null || true
360
+ # Add HTTP MCP server
361
+ claude mcp add ctlsurf -s user --transport http "$MCP_URL" \
362
+ --header "Authorization: Bearer $API_KEY" 2>/dev/null || true
363
+
364
+ # Verify the config was actually persisted
365
+ CLAUDE_SETTINGS="$HOME/.claude/settings.json"
366
+ CLAUDE_CONFIG_OK=false
367
+ if [ -f "$CLAUDE_SETTINGS" ] && command -v jq &> /dev/null; then
368
+ if jq -e '.mcpServers.ctlsurf.url' "$CLAUDE_SETTINGS" &>/dev/null; then
369
+ CLAUDE_CONFIG_OK=true
370
+ fi
371
+ elif [ -f "$CLAUDE_SETTINGS" ] && grep -q "ctlsurf" "$CLAUDE_SETTINGS" 2>/dev/null; then
372
+ CLAUDE_CONFIG_OK=true
373
+ fi
374
+
375
+ # Fallback: write directly to settings.json if claude mcp add didn't persist
376
+ if [ "$CLAUDE_CONFIG_OK" = false ]; then
377
+ echo -e "${YELLOW} claude mcp add didn't persist config, writing directly...${NC}"
378
+ mkdir -p "$HOME/.claude"
379
+
380
+ if command -v jq &> /dev/null; then
381
+ # Merge into existing settings.json using jq
382
+ if [ ! -f "$CLAUDE_SETTINGS" ] || [ ! -s "$CLAUDE_SETTINGS" ] || [ "$(cat "$CLAUDE_SETTINGS")" = "{}" ]; then
383
+ echo '{}' > "$CLAUDE_SETTINGS"
384
+ fi
385
+ TEMP_CLAUDE=$(mktemp)
386
+ MCP_CONFIG=$(cat <<EOF
387
+ {
388
+ "type": "http",
389
+ "url": "$MCP_URL",
390
+ "headers": {
391
+ "Authorization": "Bearer $API_KEY"
392
+ }
393
+ }
394
+ EOF
395
+ )
396
+ jq --argjson mcpConfig "$MCP_CONFIG" \
397
+ '.mcpServers.ctlsurf = $mcpConfig' \
398
+ "$CLAUDE_SETTINGS" > "$TEMP_CLAUDE" 2>/dev/null || \
399
+ jq --argjson mcpConfig "$MCP_CONFIG" \
400
+ '. + {"mcpServers": {"ctlsurf": $mcpConfig}}' \
401
+ "$CLAUDE_SETTINGS" > "$TEMP_CLAUDE"
402
+ mv "$TEMP_CLAUDE" "$CLAUDE_SETTINGS"
403
+ else
404
+ # No jq - write settings.json directly (only if empty or missing)
405
+ if [ ! -f "$CLAUDE_SETTINGS" ] || [ ! -s "$CLAUDE_SETTINGS" ] || [ "$(cat "$CLAUDE_SETTINGS")" = "{}" ]; then
406
+ cat > "$CLAUDE_SETTINGS" << EOF
407
+ {
408
+ "mcpServers": {
409
+ "ctlsurf": {
410
+ "type": "http",
411
+ "url": "$MCP_URL",
412
+ "headers": {
413
+ "Authorization": "Bearer $API_KEY"
414
+ }
415
+ }
416
+ }
417
+ }
418
+ EOF
419
+ else
420
+ echo -e "${YELLOW} ⚠ settings.json has existing config and jq is not available${NC}"
421
+ echo " Please manually add ctlsurf to: $CLAUDE_SETTINGS"
422
+ fi
423
+ fi
424
+ fi
425
+
426
+ echo -e "${GREEN}✓ Claude Code CLI configured${NC}"
427
+ CONFIGURED_COUNT=$((CONFIGURED_COUNT + 1))
428
+ CONFIGURED_TOOLS="$CONFIGURED_TOOLS\n - Claude Code CLI: Restart terminal to use"
429
+ fi
430
+
431
+ # ============================================================
432
+ # Configure Claude Code VSCode Extension
433
+ # ============================================================
434
+ if [ "$FOUND_CLAUDE_VSCODE" = true ]; then
435
+ echo "Setting up Claude Code VSCode Extension..."
436
+
437
+ VSCODE_SETTINGS=$(get_vscode_settings_path)
438
+
439
+ if [ -n "$VSCODE_SETTINGS" ]; then
440
+ # Create settings directory if it doesn't exist
441
+ mkdir -p "$(dirname "$VSCODE_SETTINGS")"
442
+
443
+ # Create empty settings if file doesn't exist
444
+ if [ ! -f "$VSCODE_SETTINGS" ]; then
445
+ echo "{}" > "$VSCODE_SETTINGS"
446
+ fi
447
+
448
+ # Check if jq is available for safe JSON manipulation
449
+ if command -v jq &> /dev/null; then
450
+ # Use jq to safely update settings
451
+ TEMP_SETTINGS=$(mktemp)
452
+
453
+ # Create the MCP server config
454
+ MCP_CONFIG=$(cat <<EOF
455
+ {
456
+ "type": "http",
457
+ "url": "$MCP_URL",
458
+ "headers": {
459
+ "Authorization": "Bearer $API_KEY"
460
+ }
461
+ }
462
+ EOF
463
+ )
464
+ # Update settings with new MCP config
465
+ jq --argjson mcpConfig "$MCP_CONFIG" \
466
+ '.["claude-code.mcpServers"].ctlsurf = $mcpConfig' \
467
+ "$VSCODE_SETTINGS" > "$TEMP_SETTINGS" 2>/dev/null || \
468
+ jq --argjson mcpConfig "$MCP_CONFIG" \
469
+ '. + {"claude-code.mcpServers": {"ctlsurf": $mcpConfig}}' \
470
+ "$VSCODE_SETTINGS" > "$TEMP_SETTINGS"
471
+
472
+ mv "$TEMP_SETTINGS" "$VSCODE_SETTINGS"
473
+ echo -e "${GREEN}✓ Claude Code VSCode configured${NC}"
474
+ echo " Config: $VSCODE_SETTINGS"
475
+ else
476
+ # Fallback: provide manual instructions
477
+ echo -e "${YELLOW}⚠ jq not found - manual configuration required${NC}"
478
+ echo ""
479
+ echo "Add this to your VSCode settings.json:"
480
+ echo ""
481
+ echo -e "${BLUE}\"claude-code.mcpServers\": {"
482
+ echo " \"ctlsurf\": {"
483
+ echo " \"type\": \"http\","
484
+ echo " \"url\": \"$MCP_URL\","
485
+ echo " \"headers\": {"
486
+ echo " \"Authorization\": \"Bearer $API_KEY\""
487
+ echo " }"
488
+ echo " }"
489
+ echo -e "}${NC}"
490
+ echo ""
491
+ echo "Settings file: $VSCODE_SETTINGS"
492
+ fi
493
+ CONFIGURED_COUNT=$((CONFIGURED_COUNT + 1))
494
+ CONFIGURED_TOOLS="$CONFIGURED_TOOLS\n - Claude Code VSCode: Reload VSCode window"
495
+ else
496
+ echo -e "${YELLOW}⚠ Could not determine VSCode settings path${NC}"
497
+ fi
498
+ fi
499
+
500
+ # ============================================================
501
+ # Configure Codex CLI
502
+ # ============================================================
503
+ if [ "$FOUND_CODEX_CLI" = true ]; then
504
+ echo "Setting up OpenAI Codex CLI..."
505
+
506
+ CODEX_CONFIG_DIR="$HOME/.codex"
507
+ CODEX_CONFIG="$CODEX_CONFIG_DIR/config.toml"
508
+
509
+ # Create config directory if needed
510
+ mkdir -p "$CODEX_CONFIG_DIR"
511
+
512
+ # Remove existing ctlsurf config if present (use awk for reliable multi-line TOML block removal)
513
+ if [ -f "$CODEX_CONFIG" ] && grep -q "\[mcp_servers.ctlsurf\]" "$CODEX_CONFIG" 2>/dev/null; then
514
+ # awk approach: skip lines from [mcp_servers.ctlsurf] until next section or EOF
515
+ awk '
516
+ /^\[mcp_servers\.ctlsurf\]/ { skip=1; next }
517
+ /^\[/ { skip=0 }
518
+ !skip { print }
519
+ ' "$CODEX_CONFIG" > "$CODEX_CONFIG.tmp" && mv "$CODEX_CONFIG.tmp" "$CODEX_CONFIG"
520
+ fi
521
+
522
+ # Ensure config file exists
523
+ touch "$CODEX_CONFIG"
524
+
525
+ # Append ctlsurf HTTP configuration (Codex format)
526
+ cat >> "$CODEX_CONFIG" << EOF
527
+
528
+ # ctlsurf (Control Surface) MCP Server - HTTP Transport
529
+ [mcp_servers.ctlsurf]
530
+ url = "$MCP_URL"
531
+ http_headers = { "Authorization" = "Bearer $API_KEY" }
532
+ EOF
533
+
534
+ echo -e "${GREEN}✓ Codex CLI configured${NC}"
535
+ echo " Config: $CODEX_CONFIG"
536
+ CONFIGURED_COUNT=$((CONFIGURED_COUNT + 1))
537
+ CONFIGURED_TOOLS="$CONFIGURED_TOOLS\n - Codex CLI: Run 'codex' and type /mcp to verify"
538
+ fi
539
+
540
+ # ============================================================
541
+ # Configure Codex/ChatGPT VSCode Extension
542
+ # ============================================================
543
+ if [ "$FOUND_CODEX_VSCODE" = true ]; then
544
+ echo "Setting up OpenAI ChatGPT VSCode Extension..."
545
+
546
+ VSCODE_SETTINGS=$(get_vscode_settings_path)
547
+
548
+ if [ -n "$VSCODE_SETTINGS" ]; then
549
+ # Create settings directory if it doesn't exist
550
+ mkdir -p "$(dirname "$VSCODE_SETTINGS")"
551
+
552
+ # Create empty settings if file doesn't exist
553
+ if [ ! -f "$VSCODE_SETTINGS" ]; then
554
+ echo "{}" > "$VSCODE_SETTINGS"
555
+ fi
556
+
557
+ # Check if jq is available for safe JSON manipulation
558
+ if command -v jq &> /dev/null; then
559
+ # Use jq to safely update settings
560
+ TEMP_SETTINGS=$(mktemp)
561
+
562
+ # Create the MCP server config for ChatGPT
563
+ MCP_CONFIG=$(cat <<EOF
564
+ {
565
+ "type": "http",
566
+ "url": "$MCP_URL",
567
+ "headers": {
568
+ "Authorization": "Bearer $API_KEY"
569
+ }
570
+ }
571
+ EOF
572
+ )
573
+ # Update settings with new MCP config (chatgpt.mcpServers)
574
+ jq --argjson mcpConfig "$MCP_CONFIG" \
575
+ '.["chatgpt.mcpServers"].ctlsurf = $mcpConfig' \
576
+ "$VSCODE_SETTINGS" > "$TEMP_SETTINGS" 2>/dev/null || \
577
+ jq --argjson mcpConfig "$MCP_CONFIG" \
578
+ '. + {"chatgpt.mcpServers": {"ctlsurf": $mcpConfig}}' \
579
+ "$VSCODE_SETTINGS" > "$TEMP_SETTINGS"
580
+
581
+ mv "$TEMP_SETTINGS" "$VSCODE_SETTINGS"
582
+ echo -e "${GREEN}✓ ChatGPT VSCode configured${NC}"
583
+ echo " Config: $VSCODE_SETTINGS"
584
+ else
585
+ # Fallback: provide manual instructions
586
+ echo -e "${YELLOW}⚠ jq not found - manual configuration required${NC}"
587
+ echo ""
588
+ echo "Add this to your VSCode settings.json:"
589
+ echo ""
590
+ echo -e "${BLUE}\"chatgpt.mcpServers\": {"
591
+ echo " \"ctlsurf\": {"
592
+ echo " \"type\": \"http\","
593
+ echo " \"url\": \"$MCP_URL\","
594
+ echo " \"headers\": {"
595
+ echo " \"Authorization\": \"Bearer $API_KEY\""
596
+ echo " }"
597
+ echo " }"
598
+ echo -e "}${NC}"
599
+ echo ""
600
+ echo "Settings file: $VSCODE_SETTINGS"
601
+ fi
602
+ CONFIGURED_COUNT=$((CONFIGURED_COUNT + 1))
603
+ CONFIGURED_TOOLS="$CONFIGURED_TOOLS\n - ChatGPT VSCode: Reload VSCode window"
604
+ else
605
+ echo -e "${YELLOW}⚠ Could not determine VSCode settings path${NC}"
606
+ fi
607
+ fi
608
+
609
+ # ============================================================
610
+ # Configure GitHub Copilot VSCode Extension
611
+ # ============================================================
612
+ if [ "$FOUND_COPILOT" = true ]; then
613
+ echo "Setting up GitHub Copilot VSCode Extension..."
614
+
615
+ VSCODE_MCP=$(get_vscode_mcp_path)
616
+
617
+ if [ -n "$VSCODE_MCP" ]; then
618
+ # Create config directory if it doesn't exist
619
+ mkdir -p "$(dirname "$VSCODE_MCP")"
620
+
621
+ # Create empty mcp.json if file doesn't exist
622
+ if [ ! -f "$VSCODE_MCP" ]; then
623
+ echo '{"servers":{}}' > "$VSCODE_MCP"
624
+ fi
625
+
626
+ # Check if jq is available for safe JSON manipulation
627
+ if command -v jq &> /dev/null; then
628
+ # Use jq to safely update mcp.json
629
+ TEMP_MCP=$(mktemp)
630
+
631
+ # Create the MCP server config for Copilot
632
+ MCP_CONFIG=$(cat <<EOF
633
+ {
634
+ "type": "http",
635
+ "url": "$MCP_URL",
636
+ "headers": {
637
+ "Authorization": "Bearer $API_KEY"
638
+ }
639
+ }
640
+ EOF
641
+ )
642
+ # Update mcp.json with new config
643
+ # Copilot uses { "servers": { "name": { ... } } } format
644
+ jq --argjson mcpConfig "$MCP_CONFIG" \
645
+ '.servers.ctlsurf = $mcpConfig' \
646
+ "$VSCODE_MCP" > "$TEMP_MCP" 2>/dev/null || \
647
+ jq --argjson mcpConfig "$MCP_CONFIG" \
648
+ '. + {"servers": {"ctlsurf": $mcpConfig}}' \
649
+ "$VSCODE_MCP" > "$TEMP_MCP"
650
+
651
+ mv "$TEMP_MCP" "$VSCODE_MCP"
652
+ echo -e "${GREEN}✓ GitHub Copilot configured${NC}"
653
+ echo " Config: $VSCODE_MCP"
654
+ else
655
+ # Fallback: provide manual instructions
656
+ echo -e "${YELLOW}⚠ jq not found - manual configuration required${NC}"
657
+ echo ""
658
+ echo "Create or edit: $VSCODE_MCP"
659
+ echo ""
660
+ echo -e "${BLUE}{"
661
+ echo " \"servers\": {"
662
+ echo " \"ctlsurf\": {"
663
+ echo " \"type\": \"http\","
664
+ echo " \"url\": \"$MCP_URL\","
665
+ echo " \"headers\": {"
666
+ echo " \"Authorization\": \"Bearer $API_KEY\""
667
+ echo " }"
668
+ echo " }"
669
+ echo " }"
670
+ echo -e "}${NC}"
671
+ fi
672
+ CONFIGURED_COUNT=$((CONFIGURED_COUNT + 1))
673
+ CONFIGURED_TOOLS="$CONFIGURED_TOOLS\n - GitHub Copilot: Reload VSCode window"
674
+ else
675
+ echo -e "${YELLOW}⚠ Could not determine VSCode mcp.json path${NC}"
676
+ fi
677
+ fi
678
+
679
+ # ============================================================
680
+ # Configure Cursor Editor
681
+ # ============================================================
682
+ if [ "$FOUND_CURSOR" = true ]; then
683
+ echo "Setting up Cursor Editor..."
684
+
685
+ CURSOR_CONFIG_DIR="$HOME/.cursor"
686
+ CURSOR_MCP="$CURSOR_CONFIG_DIR/mcp.json"
687
+
688
+ # Create config directory if needed
689
+ mkdir -p "$CURSOR_CONFIG_DIR"
690
+
691
+ # Create empty mcp.json if file doesn't exist
692
+ if [ ! -f "$CURSOR_MCP" ]; then
693
+ echo '{"mcpServers":{}}' > "$CURSOR_MCP"
694
+ fi
695
+
696
+ # Check if jq is available for safe JSON manipulation
697
+ if command -v jq &> /dev/null; then
698
+ # Use jq to safely update mcp.json
699
+ TEMP_MCP=$(mktemp)
700
+
701
+ # Create the MCP server config for Cursor
702
+ # Cursor uses { "mcpServers": { "name": { url, headers } } } format (no "type" field for HTTP)
703
+ MCP_CONFIG=$(cat <<EOF
704
+ {
705
+ "url": "$MCP_URL",
706
+ "headers": {
707
+ "Authorization": "Bearer $API_KEY"
708
+ }
709
+ }
710
+ EOF
711
+ )
712
+ # Update mcp.json with new config
713
+ jq --argjson mcpConfig "$MCP_CONFIG" \
714
+ '.mcpServers.ctlsurf = $mcpConfig' \
715
+ "$CURSOR_MCP" > "$TEMP_MCP" 2>/dev/null || \
716
+ jq --argjson mcpConfig "$MCP_CONFIG" \
717
+ '. + {"mcpServers": {"ctlsurf": $mcpConfig}}' \
718
+ "$CURSOR_MCP" > "$TEMP_MCP"
719
+
720
+ mv "$TEMP_MCP" "$CURSOR_MCP"
721
+ echo -e "${GREEN}✓ Cursor configured${NC}"
722
+ echo " Config: $CURSOR_MCP"
723
+ else
724
+ # Fallback: provide manual instructions
725
+ echo -e "${YELLOW}⚠ jq not found - manual configuration required${NC}"
726
+ echo ""
727
+ echo "Create or edit: $CURSOR_MCP"
728
+ echo ""
729
+ echo -e "${BLUE}{"
730
+ echo " \"mcpServers\": {"
731
+ echo " \"ctlsurf\": {"
732
+ echo " \"url\": \"$MCP_URL\","
733
+ echo " \"headers\": {"
734
+ echo " \"Authorization\": \"Bearer $API_KEY\""
735
+ echo " }"
736
+ echo " }"
737
+ echo " }"
738
+ echo -e "}${NC}"
739
+ fi
740
+ CONFIGURED_COUNT=$((CONFIGURED_COUNT + 1))
741
+ CONFIGURED_TOOLS="$CONFIGURED_TOOLS\n - Cursor: Restart Cursor to use"
742
+ fi
743
+
744
+ echo ""
745
+ echo "========================================"
746
+ echo -e "${GREEN} Setup Complete!${NC}"
747
+ echo "========================================"
748
+ echo ""
749
+ echo "Server: $SERVER_URL"
750
+ echo "Endpoint: $MCP_URL"
751
+ echo "Tools configured: $CONFIGURED_COUNT"
752
+ echo ""
753
+
754
+ if [ -n "$SHELL_PROFILE" ]; then
755
+ echo "To use the API key in current shell:"
756
+ echo " source $SHELL_PROFILE"
757
+ echo ""
758
+ fi
759
+
760
+ if [ -n "$CONFIGURED_TOOLS" ]; then
761
+ echo "Next steps:"
762
+ echo -e "$CONFIGURED_TOOLS"
763
+ echo ""
764
+ fi