@phenx-inc/ctlsurf 0.1.0 → 0.1.1

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) {
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.1",
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,696 @@
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
+ # Get API key (read from /dev/tty for curl|bash compatibility)
203
+ echo "Get your API key from: https://app.ctlsurf.com/settings (API Keys tab)"
204
+ echo -n "Enter your ctlsurf API key: "
205
+ read API_KEY < /dev/tty
206
+
207
+ if [ -z "$API_KEY" ]; then
208
+ echo -e "${RED}ERROR: API key is required${NC}"
209
+ exit 1
210
+ fi
211
+
212
+ echo ""
213
+ echo -n "Server URL [https://app.ctlsurf.com]: "
214
+ read SERVER_URL < /dev/tty
215
+ SERVER_URL=${SERVER_URL:-https://app.ctlsurf.com}
216
+ # Normalize URL
217
+ SERVER_URL="${SERVER_URL%/}"
218
+ SERVER_URL="${SERVER_URL%/api}"
219
+ SERVER_URL="${SERVER_URL%/api/mcp}"
220
+ MCP_URL="${SERVER_URL}/api/mcp"
221
+
222
+ echo ""
223
+ echo "Configuring ctlsurf..."
224
+ echo ""
225
+
226
+ # Persist API key to shell profile
227
+ SHELL_PROFILE=""
228
+ if [ -f "$HOME/.zshrc" ]; then
229
+ SHELL_PROFILE="$HOME/.zshrc"
230
+ elif [ -f "$HOME/.bashrc" ]; then
231
+ SHELL_PROFILE="$HOME/.bashrc"
232
+ elif [ -f "$HOME/.bash_profile" ]; then
233
+ SHELL_PROFILE="$HOME/.bash_profile"
234
+ fi
235
+
236
+ if [ -n "$SHELL_PROFILE" ]; then
237
+ # Remove existing CTLSURF exports
238
+ if [[ "$OSTYPE" == "darwin"* ]]; then
239
+ sed -i '' '/^export CTLSURF_/d' "$SHELL_PROFILE" 2>/dev/null || true
240
+ else
241
+ sed -i '/^export CTLSURF_/d' "$SHELL_PROFILE" 2>/dev/null || true
242
+ fi
243
+
244
+ # Add new exports
245
+ echo "" >> "$SHELL_PROFILE"
246
+ echo "# ctlsurf MCP configuration" >> "$SHELL_PROFILE"
247
+ echo "export CTLSURF_API_KEY=\"$API_KEY\"" >> "$SHELL_PROFILE"
248
+ echo "export CTLSURF_URL=\"$MCP_URL\"" >> "$SHELL_PROFILE"
249
+
250
+ echo -e "${GREEN}✓ Added API key to $SHELL_PROFILE${NC}"
251
+ echo ""
252
+ fi
253
+
254
+ CONFIGURED_COUNT=0
255
+ CONFIGURED_TOOLS=""
256
+
257
+ # ============================================================
258
+ # Configure Claude Code CLI
259
+ # ============================================================
260
+ if [ "$FOUND_CLAUDE_CLI" = true ]; then
261
+ echo "Setting up Claude Code CLI..."
262
+
263
+ # Check for stale .mcp.json files that could shadow the user-level config
264
+ for MCP_JSON_PATH in "$HOME/.mcp.json" "./.mcp.json"; do
265
+ if [ -f "$MCP_JSON_PATH" ] && grep -q "ctlsurf" "$MCP_JSON_PATH" 2>/dev/null; then
266
+ # Check if it has an old stdio-based config (command-based, not HTTP)
267
+ if grep -q '"command"' "$MCP_JSON_PATH" 2>/dev/null && ! grep -q '"type".*"http"' "$MCP_JSON_PATH" 2>/dev/null; then
268
+ echo -e "${YELLOW} ⚠ Found stale ctlsurf config in $MCP_JSON_PATH (old stdio transport)${NC}"
269
+ echo -e "${YELLOW} This will shadow the HTTP config and cause connection failures.${NC}"
270
+ if command -v jq &> /dev/null; then
271
+ # Remove ctlsurf entry from .mcp.json using jq
272
+ TEMP_MCP_JSON=$(mktemp)
273
+ jq 'del(.mcpServers.ctlsurf)' "$MCP_JSON_PATH" > "$TEMP_MCP_JSON" 2>/dev/null
274
+ # If .mcp.json is now empty (no other servers), remove the file
275
+ if jq -e '.mcpServers | length == 0' "$TEMP_MCP_JSON" &>/dev/null; then
276
+ rm "$MCP_JSON_PATH"
277
+ rm "$TEMP_MCP_JSON"
278
+ echo -e "${GREEN} Removed empty $MCP_JSON_PATH${NC}"
279
+ else
280
+ mv "$TEMP_MCP_JSON" "$MCP_JSON_PATH"
281
+ echo -e "${GREEN} Removed stale ctlsurf entry from $MCP_JSON_PATH${NC}"
282
+ fi
283
+ else
284
+ echo -e "${YELLOW} Please remove ctlsurf from $MCP_JSON_PATH manually${NC}"
285
+ fi
286
+ fi
287
+ fi
288
+ done
289
+
290
+ # Remove existing config
291
+ claude mcp remove ctlsurf -s user 2>/dev/null || true
292
+ # Add HTTP MCP server
293
+ claude mcp add ctlsurf -s user --transport http "$MCP_URL" \
294
+ --header "Authorization: Bearer $API_KEY" 2>/dev/null || true
295
+
296
+ # Verify the config was actually persisted
297
+ CLAUDE_SETTINGS="$HOME/.claude/settings.json"
298
+ CLAUDE_CONFIG_OK=false
299
+ if [ -f "$CLAUDE_SETTINGS" ] && command -v jq &> /dev/null; then
300
+ if jq -e '.mcpServers.ctlsurf.url' "$CLAUDE_SETTINGS" &>/dev/null; then
301
+ CLAUDE_CONFIG_OK=true
302
+ fi
303
+ elif [ -f "$CLAUDE_SETTINGS" ] && grep -q "ctlsurf" "$CLAUDE_SETTINGS" 2>/dev/null; then
304
+ CLAUDE_CONFIG_OK=true
305
+ fi
306
+
307
+ # Fallback: write directly to settings.json if claude mcp add didn't persist
308
+ if [ "$CLAUDE_CONFIG_OK" = false ]; then
309
+ echo -e "${YELLOW} claude mcp add didn't persist config, writing directly...${NC}"
310
+ mkdir -p "$HOME/.claude"
311
+
312
+ if command -v jq &> /dev/null; then
313
+ # Merge into existing settings.json using jq
314
+ if [ ! -f "$CLAUDE_SETTINGS" ] || [ ! -s "$CLAUDE_SETTINGS" ] || [ "$(cat "$CLAUDE_SETTINGS")" = "{}" ]; then
315
+ echo '{}' > "$CLAUDE_SETTINGS"
316
+ fi
317
+ TEMP_CLAUDE=$(mktemp)
318
+ MCP_CONFIG=$(cat <<EOF
319
+ {
320
+ "type": "http",
321
+ "url": "$MCP_URL",
322
+ "headers": {
323
+ "Authorization": "Bearer $API_KEY"
324
+ }
325
+ }
326
+ EOF
327
+ )
328
+ jq --argjson mcpConfig "$MCP_CONFIG" \
329
+ '.mcpServers.ctlsurf = $mcpConfig' \
330
+ "$CLAUDE_SETTINGS" > "$TEMP_CLAUDE" 2>/dev/null || \
331
+ jq --argjson mcpConfig "$MCP_CONFIG" \
332
+ '. + {"mcpServers": {"ctlsurf": $mcpConfig}}' \
333
+ "$CLAUDE_SETTINGS" > "$TEMP_CLAUDE"
334
+ mv "$TEMP_CLAUDE" "$CLAUDE_SETTINGS"
335
+ else
336
+ # No jq - write settings.json directly (only if empty or missing)
337
+ if [ ! -f "$CLAUDE_SETTINGS" ] || [ ! -s "$CLAUDE_SETTINGS" ] || [ "$(cat "$CLAUDE_SETTINGS")" = "{}" ]; then
338
+ cat > "$CLAUDE_SETTINGS" << EOF
339
+ {
340
+ "mcpServers": {
341
+ "ctlsurf": {
342
+ "type": "http",
343
+ "url": "$MCP_URL",
344
+ "headers": {
345
+ "Authorization": "Bearer $API_KEY"
346
+ }
347
+ }
348
+ }
349
+ }
350
+ EOF
351
+ else
352
+ echo -e "${YELLOW} ⚠ settings.json has existing config and jq is not available${NC}"
353
+ echo " Please manually add ctlsurf to: $CLAUDE_SETTINGS"
354
+ fi
355
+ fi
356
+ fi
357
+
358
+ echo -e "${GREEN}✓ Claude Code CLI configured${NC}"
359
+ CONFIGURED_COUNT=$((CONFIGURED_COUNT + 1))
360
+ CONFIGURED_TOOLS="$CONFIGURED_TOOLS\n - Claude Code CLI: Restart terminal to use"
361
+ fi
362
+
363
+ # ============================================================
364
+ # Configure Claude Code VSCode Extension
365
+ # ============================================================
366
+ if [ "$FOUND_CLAUDE_VSCODE" = true ]; then
367
+ echo "Setting up Claude Code VSCode Extension..."
368
+
369
+ VSCODE_SETTINGS=$(get_vscode_settings_path)
370
+
371
+ if [ -n "$VSCODE_SETTINGS" ]; then
372
+ # Create settings directory if it doesn't exist
373
+ mkdir -p "$(dirname "$VSCODE_SETTINGS")"
374
+
375
+ # Create empty settings if file doesn't exist
376
+ if [ ! -f "$VSCODE_SETTINGS" ]; then
377
+ echo "{}" > "$VSCODE_SETTINGS"
378
+ fi
379
+
380
+ # Check if jq is available for safe JSON manipulation
381
+ if command -v jq &> /dev/null; then
382
+ # Use jq to safely update settings
383
+ TEMP_SETTINGS=$(mktemp)
384
+
385
+ # Create the MCP server config
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
+ # Update settings with new MCP config
397
+ jq --argjson mcpConfig "$MCP_CONFIG" \
398
+ '.["claude-code.mcpServers"].ctlsurf = $mcpConfig' \
399
+ "$VSCODE_SETTINGS" > "$TEMP_SETTINGS" 2>/dev/null || \
400
+ jq --argjson mcpConfig "$MCP_CONFIG" \
401
+ '. + {"claude-code.mcpServers": {"ctlsurf": $mcpConfig}}' \
402
+ "$VSCODE_SETTINGS" > "$TEMP_SETTINGS"
403
+
404
+ mv "$TEMP_SETTINGS" "$VSCODE_SETTINGS"
405
+ echo -e "${GREEN}✓ Claude Code VSCode configured${NC}"
406
+ echo " Config: $VSCODE_SETTINGS"
407
+ else
408
+ # Fallback: provide manual instructions
409
+ echo -e "${YELLOW}⚠ jq not found - manual configuration required${NC}"
410
+ echo ""
411
+ echo "Add this to your VSCode settings.json:"
412
+ echo ""
413
+ echo -e "${BLUE}\"claude-code.mcpServers\": {"
414
+ echo " \"ctlsurf\": {"
415
+ echo " \"type\": \"http\","
416
+ echo " \"url\": \"$MCP_URL\","
417
+ echo " \"headers\": {"
418
+ echo " \"Authorization\": \"Bearer $API_KEY\""
419
+ echo " }"
420
+ echo " }"
421
+ echo -e "}${NC}"
422
+ echo ""
423
+ echo "Settings file: $VSCODE_SETTINGS"
424
+ fi
425
+ CONFIGURED_COUNT=$((CONFIGURED_COUNT + 1))
426
+ CONFIGURED_TOOLS="$CONFIGURED_TOOLS\n - Claude Code VSCode: Reload VSCode window"
427
+ else
428
+ echo -e "${YELLOW}⚠ Could not determine VSCode settings path${NC}"
429
+ fi
430
+ fi
431
+
432
+ # ============================================================
433
+ # Configure Codex CLI
434
+ # ============================================================
435
+ if [ "$FOUND_CODEX_CLI" = true ]; then
436
+ echo "Setting up OpenAI Codex CLI..."
437
+
438
+ CODEX_CONFIG_DIR="$HOME/.codex"
439
+ CODEX_CONFIG="$CODEX_CONFIG_DIR/config.toml"
440
+
441
+ # Create config directory if needed
442
+ mkdir -p "$CODEX_CONFIG_DIR"
443
+
444
+ # Remove existing ctlsurf config if present (use awk for reliable multi-line TOML block removal)
445
+ if [ -f "$CODEX_CONFIG" ] && grep -q "\[mcp_servers.ctlsurf\]" "$CODEX_CONFIG" 2>/dev/null; then
446
+ # awk approach: skip lines from [mcp_servers.ctlsurf] until next section or EOF
447
+ awk '
448
+ /^\[mcp_servers\.ctlsurf\]/ { skip=1; next }
449
+ /^\[/ { skip=0 }
450
+ !skip { print }
451
+ ' "$CODEX_CONFIG" > "$CODEX_CONFIG.tmp" && mv "$CODEX_CONFIG.tmp" "$CODEX_CONFIG"
452
+ fi
453
+
454
+ # Ensure config file exists
455
+ touch "$CODEX_CONFIG"
456
+
457
+ # Append ctlsurf HTTP configuration (Codex format)
458
+ cat >> "$CODEX_CONFIG" << EOF
459
+
460
+ # ctlsurf (Control Surface) MCP Server - HTTP Transport
461
+ [mcp_servers.ctlsurf]
462
+ url = "$MCP_URL"
463
+ http_headers = { "Authorization" = "Bearer $API_KEY" }
464
+ EOF
465
+
466
+ echo -e "${GREEN}✓ Codex CLI configured${NC}"
467
+ echo " Config: $CODEX_CONFIG"
468
+ CONFIGURED_COUNT=$((CONFIGURED_COUNT + 1))
469
+ CONFIGURED_TOOLS="$CONFIGURED_TOOLS\n - Codex CLI: Run 'codex' and type /mcp to verify"
470
+ fi
471
+
472
+ # ============================================================
473
+ # Configure Codex/ChatGPT VSCode Extension
474
+ # ============================================================
475
+ if [ "$FOUND_CODEX_VSCODE" = true ]; then
476
+ echo "Setting up OpenAI ChatGPT VSCode Extension..."
477
+
478
+ VSCODE_SETTINGS=$(get_vscode_settings_path)
479
+
480
+ if [ -n "$VSCODE_SETTINGS" ]; then
481
+ # Create settings directory if it doesn't exist
482
+ mkdir -p "$(dirname "$VSCODE_SETTINGS")"
483
+
484
+ # Create empty settings if file doesn't exist
485
+ if [ ! -f "$VSCODE_SETTINGS" ]; then
486
+ echo "{}" > "$VSCODE_SETTINGS"
487
+ fi
488
+
489
+ # Check if jq is available for safe JSON manipulation
490
+ if command -v jq &> /dev/null; then
491
+ # Use jq to safely update settings
492
+ TEMP_SETTINGS=$(mktemp)
493
+
494
+ # Create the MCP server config for ChatGPT
495
+ MCP_CONFIG=$(cat <<EOF
496
+ {
497
+ "type": "http",
498
+ "url": "$MCP_URL",
499
+ "headers": {
500
+ "Authorization": "Bearer $API_KEY"
501
+ }
502
+ }
503
+ EOF
504
+ )
505
+ # Update settings with new MCP config (chatgpt.mcpServers)
506
+ jq --argjson mcpConfig "$MCP_CONFIG" \
507
+ '.["chatgpt.mcpServers"].ctlsurf = $mcpConfig' \
508
+ "$VSCODE_SETTINGS" > "$TEMP_SETTINGS" 2>/dev/null || \
509
+ jq --argjson mcpConfig "$MCP_CONFIG" \
510
+ '. + {"chatgpt.mcpServers": {"ctlsurf": $mcpConfig}}' \
511
+ "$VSCODE_SETTINGS" > "$TEMP_SETTINGS"
512
+
513
+ mv "$TEMP_SETTINGS" "$VSCODE_SETTINGS"
514
+ echo -e "${GREEN}✓ ChatGPT VSCode configured${NC}"
515
+ echo " Config: $VSCODE_SETTINGS"
516
+ else
517
+ # Fallback: provide manual instructions
518
+ echo -e "${YELLOW}⚠ jq not found - manual configuration required${NC}"
519
+ echo ""
520
+ echo "Add this to your VSCode settings.json:"
521
+ echo ""
522
+ echo -e "${BLUE}\"chatgpt.mcpServers\": {"
523
+ echo " \"ctlsurf\": {"
524
+ echo " \"type\": \"http\","
525
+ echo " \"url\": \"$MCP_URL\","
526
+ echo " \"headers\": {"
527
+ echo " \"Authorization\": \"Bearer $API_KEY\""
528
+ echo " }"
529
+ echo " }"
530
+ echo -e "}${NC}"
531
+ echo ""
532
+ echo "Settings file: $VSCODE_SETTINGS"
533
+ fi
534
+ CONFIGURED_COUNT=$((CONFIGURED_COUNT + 1))
535
+ CONFIGURED_TOOLS="$CONFIGURED_TOOLS\n - ChatGPT VSCode: Reload VSCode window"
536
+ else
537
+ echo -e "${YELLOW}⚠ Could not determine VSCode settings path${NC}"
538
+ fi
539
+ fi
540
+
541
+ # ============================================================
542
+ # Configure GitHub Copilot VSCode Extension
543
+ # ============================================================
544
+ if [ "$FOUND_COPILOT" = true ]; then
545
+ echo "Setting up GitHub Copilot VSCode Extension..."
546
+
547
+ VSCODE_MCP=$(get_vscode_mcp_path)
548
+
549
+ if [ -n "$VSCODE_MCP" ]; then
550
+ # Create config directory if it doesn't exist
551
+ mkdir -p "$(dirname "$VSCODE_MCP")"
552
+
553
+ # Create empty mcp.json if file doesn't exist
554
+ if [ ! -f "$VSCODE_MCP" ]; then
555
+ echo '{"servers":{}}' > "$VSCODE_MCP"
556
+ fi
557
+
558
+ # Check if jq is available for safe JSON manipulation
559
+ if command -v jq &> /dev/null; then
560
+ # Use jq to safely update mcp.json
561
+ TEMP_MCP=$(mktemp)
562
+
563
+ # Create the MCP server config for Copilot
564
+ MCP_CONFIG=$(cat <<EOF
565
+ {
566
+ "type": "http",
567
+ "url": "$MCP_URL",
568
+ "headers": {
569
+ "Authorization": "Bearer $API_KEY"
570
+ }
571
+ }
572
+ EOF
573
+ )
574
+ # Update mcp.json with new config
575
+ # Copilot uses { "servers": { "name": { ... } } } format
576
+ jq --argjson mcpConfig "$MCP_CONFIG" \
577
+ '.servers.ctlsurf = $mcpConfig' \
578
+ "$VSCODE_MCP" > "$TEMP_MCP" 2>/dev/null || \
579
+ jq --argjson mcpConfig "$MCP_CONFIG" \
580
+ '. + {"servers": {"ctlsurf": $mcpConfig}}' \
581
+ "$VSCODE_MCP" > "$TEMP_MCP"
582
+
583
+ mv "$TEMP_MCP" "$VSCODE_MCP"
584
+ echo -e "${GREEN}✓ GitHub Copilot configured${NC}"
585
+ echo " Config: $VSCODE_MCP"
586
+ else
587
+ # Fallback: provide manual instructions
588
+ echo -e "${YELLOW}⚠ jq not found - manual configuration required${NC}"
589
+ echo ""
590
+ echo "Create or edit: $VSCODE_MCP"
591
+ echo ""
592
+ echo -e "${BLUE}{"
593
+ echo " \"servers\": {"
594
+ echo " \"ctlsurf\": {"
595
+ echo " \"type\": \"http\","
596
+ echo " \"url\": \"$MCP_URL\","
597
+ echo " \"headers\": {"
598
+ echo " \"Authorization\": \"Bearer $API_KEY\""
599
+ echo " }"
600
+ echo " }"
601
+ echo " }"
602
+ echo -e "}${NC}"
603
+ fi
604
+ CONFIGURED_COUNT=$((CONFIGURED_COUNT + 1))
605
+ CONFIGURED_TOOLS="$CONFIGURED_TOOLS\n - GitHub Copilot: Reload VSCode window"
606
+ else
607
+ echo -e "${YELLOW}⚠ Could not determine VSCode mcp.json path${NC}"
608
+ fi
609
+ fi
610
+
611
+ # ============================================================
612
+ # Configure Cursor Editor
613
+ # ============================================================
614
+ if [ "$FOUND_CURSOR" = true ]; then
615
+ echo "Setting up Cursor Editor..."
616
+
617
+ CURSOR_CONFIG_DIR="$HOME/.cursor"
618
+ CURSOR_MCP="$CURSOR_CONFIG_DIR/mcp.json"
619
+
620
+ # Create config directory if needed
621
+ mkdir -p "$CURSOR_CONFIG_DIR"
622
+
623
+ # Create empty mcp.json if file doesn't exist
624
+ if [ ! -f "$CURSOR_MCP" ]; then
625
+ echo '{"mcpServers":{}}' > "$CURSOR_MCP"
626
+ fi
627
+
628
+ # Check if jq is available for safe JSON manipulation
629
+ if command -v jq &> /dev/null; then
630
+ # Use jq to safely update mcp.json
631
+ TEMP_MCP=$(mktemp)
632
+
633
+ # Create the MCP server config for Cursor
634
+ # Cursor uses { "mcpServers": { "name": { url, headers } } } format (no "type" field for HTTP)
635
+ MCP_CONFIG=$(cat <<EOF
636
+ {
637
+ "url": "$MCP_URL",
638
+ "headers": {
639
+ "Authorization": "Bearer $API_KEY"
640
+ }
641
+ }
642
+ EOF
643
+ )
644
+ # Update mcp.json with new config
645
+ jq --argjson mcpConfig "$MCP_CONFIG" \
646
+ '.mcpServers.ctlsurf = $mcpConfig' \
647
+ "$CURSOR_MCP" > "$TEMP_MCP" 2>/dev/null || \
648
+ jq --argjson mcpConfig "$MCP_CONFIG" \
649
+ '. + {"mcpServers": {"ctlsurf": $mcpConfig}}' \
650
+ "$CURSOR_MCP" > "$TEMP_MCP"
651
+
652
+ mv "$TEMP_MCP" "$CURSOR_MCP"
653
+ echo -e "${GREEN}✓ Cursor configured${NC}"
654
+ echo " Config: $CURSOR_MCP"
655
+ else
656
+ # Fallback: provide manual instructions
657
+ echo -e "${YELLOW}⚠ jq not found - manual configuration required${NC}"
658
+ echo ""
659
+ echo "Create or edit: $CURSOR_MCP"
660
+ echo ""
661
+ echo -e "${BLUE}{"
662
+ echo " \"mcpServers\": {"
663
+ echo " \"ctlsurf\": {"
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 - Cursor: Restart Cursor to use"
674
+ fi
675
+
676
+ echo ""
677
+ echo "========================================"
678
+ echo -e "${GREEN} Setup Complete!${NC}"
679
+ echo "========================================"
680
+ echo ""
681
+ echo "Server: $SERVER_URL"
682
+ echo "Endpoint: $MCP_URL"
683
+ echo "Tools configured: $CONFIGURED_COUNT"
684
+ echo ""
685
+
686
+ if [ -n "$SHELL_PROFILE" ]; then
687
+ echo "To use the API key in current shell:"
688
+ echo " source $SHELL_PROFILE"
689
+ echo ""
690
+ fi
691
+
692
+ if [ -n "$CONFIGURED_TOOLS" ]; then
693
+ echo "Next steps:"
694
+ echo -e "$CONFIGURED_TOOLS"
695
+ echo ""
696
+ fi