#!/bin/sh # Connects The Padam (Tamil and Malayalam films for your mood) to the AI agents on this computer: # # curl -fsSL https://thepadam.com/ai.sh | sh # # It adds The Padam's MCP server, https://thepadam.com/mcp (read-only, no sign-in), to every agent it # finds: Claude Code, Codex, Gemini CLI and VS Code with their own commands, and Cursor and # Windsurf through their settings files. Existing editor settings are backed up before merging. # Nothing else is installed, nothing needs sudo, and existing settings are preserved. set -u NAME=the-padam URL='https://thepadam.com/mcp' JSON="{\"name\":\"$NAME\",\"type\":\"http\",\"url\":\"$URL\"}" found=0 failed=0 have() { command -v "$1" >/dev/null 2>&1; } added() { echo " ✓ $1: configured"; found=$((found + 1)); } already() { echo " ✓ $1: already configured"; found=$((found + 1)); } manual() { echo " ✗ $1: could not connect. Run this yourself:"; echo " $2"; failed=$((failed + 1)); } conflict() { echo " ✗ $1: $NAME is a different server. Check it with: $2"; failed=$((failed + 1)); } # Exit codes: 0 = added/missing, 10 = already configured, 11 = conflicting server, # 12 = cannot safely read/write. Existing files need an installed JSON parser. json_config() { # mode, path, root key, endpoint field, new entry if [ ! -e "$2" ] && [ ! -L "$2" ]; then [ "$1" = inspect ] && return 0 (umask 077; set -C; printf '{"%s": {"%s": %s}}\n' "$3" "$NAME" "$5" > "$2") && return 0 return 12 fi if have node; then node --input-type=module - "$1" "$2" "$3" "$4" "$5" "$NAME" "$URL" <<'JS' import fs from 'node:fs'; import {randomBytes} from 'node:crypto'; const [mode, path, root, field, entry, name, url] = process.argv.slice(2); const object = value => value !== null && typeof value === 'object' && !Array.isArray(value); let stage; try { if (fs.lstatSync(path).isSymbolicLink()) process.exit(12); const original = fs.readFileSync(path); // JSONC: remove comments/trailing commas only outside quoted strings. const clean = original.toString('utf8') .replace(/"(?:\\.|[^"\\])*"|\/\/[^\r\n]*|\/\*[\s\S]*?\*\//g, token => token.startsWith('"') ? token : ' ') .replace(/"(?:\\.|[^"\\])*"|,\s*(?=[}\]])/g, token => token.startsWith('"') ? token : ''); const data = JSON.parse(clean); if (!object(data) || (root in data && !object(data[root]))) process.exit(12); const servers = data[root] ?? {}; if (Object.hasOwn(servers, name)) { const saved = servers[name]; const endpointMatches = object(saved) && (saved[field] === url || (field === 'httpUrl' && saved.url === url && saved.type === 'http')); process.exit(endpointMatches && !('command' in saved) && (saved.type === undefined || saved.type === 'http') ? 10 : 11); } if (mode === 'inspect') process.exit(0); data[root] = {...servers, [name]: JSON.parse(entry)}; const suffix = randomBytes(8).toString('hex'); fs.writeFileSync(`${path}.padam-backup-${suffix}`, original, {flag: 'wx', mode: 0o600}); stage = `${path}.padam-tmp-${suffix}`; fs.writeFileSync(stage, JSON.stringify(data, null, 2) + '\n', {flag: 'wx', mode: fs.statSync(path).mode & 0o777}); fs.renameSync(stage, path); } catch { process.exitCode = 12; } finally { if (stage && fs.existsSync(stage)) fs.unlinkSync(stage); } JS return $? elif have python3; then python3 -I - "$1" "$2" "$3" "$4" "$5" "$NAME" "$URL" <<'PY' import json, os, re, stat, sys, tempfile from pathlib import Path mode, filename, root, field, entry, name, url = sys.argv[1:] path = Path(filename) stage = None try: if path.is_symlink(): sys.exit(12) original = path.read_bytes() clean = re.sub(r'"(?:\\.|[^"\\])*"|//[^\r\n]*|/\*[\s\S]*?\*/', lambda m: m[0] if m[0].startswith('"') else ' ', original.decode('utf-8')) clean = re.sub(r'"(?:\\.|[^"\\])*"|,\s*(?=[}\]])', lambda m: m[0] if m[0].startswith('"') else '', clean) data = json.loads(clean) if not isinstance(data, dict) or (root in data and not isinstance(data[root], dict)): sys.exit(12) servers = data.get(root, {}) if name in servers: saved = servers[name] matches = isinstance(saved, dict) and (saved.get(field) == url or (field == 'httpUrl' and saved.get('url') == url and saved.get('type') == 'http')) sys.exit(10 if matches and 'command' not in saved and saved.get('type', 'http') == 'http' else 11) if mode == 'inspect': sys.exit(0) data[root] = dict(servers, **{name: json.loads(entry)}) fd, backup = tempfile.mkstemp(prefix=path.name + '.padam-backup-', dir=path.parent) with os.fdopen(fd, 'wb') as output: output.write(original) fd, stage = tempfile.mkstemp(prefix=path.name + '.padam-tmp-', dir=path.parent) with os.fdopen(fd, 'w', encoding='utf-8') as output: json.dump(data, output, ensure_ascii=False, indent=2) output.write('\n') os.chmod(stage, stat.S_IMODE(path.stat().st_mode)) os.replace(stage, path) except (OSError, ValueError, TypeError): sys.exit(12) finally: if stage and os.path.exists(stage): os.unlink(stage) PY return $? fi return 12 } config_file() { # app, folder, file, endpoint field, new entry, root key [ -d "$2" ] || return 0 json_config merge "$2/$3" "$6" "$4" "$5" result=$? case "$result" in 0) added "$1" ;; 10) already "$1" ;; 11) conflict "$1" "$2/$3" ;; *) echo " ✗ $1: could not write $2/$3 safely. Existing settings were kept." echo " Add under \"$6\": \"$NAME\": $5" echo " Existing JSON needs Node.js or Python 3; invalid or symlinked files need a manual edit." failed=$((failed + 1)) ;; esac } echo "Connecting The Padam to the AI agents on this computer..." echo if have claude; then # A project's local server must not mask the user registration used by every project. if probe=$(mktemp -d "${TMPDIR:-/tmp}/padam-config.XXXXXXXX"); then if existing=$(cd "$probe" && claude mcp get "$NAME" /dev/null); then if printf '%s\n' "$existing" | grep -Fqx " URL: $URL"; then already "Claude Code" else conflict "Claude Code" "claude mcp get $NAME"; fi elif claude mcp add --transport http --scope user "$NAME" "$URL" /dev/null 2>&1; then added "Claude Code" else manual "Claude Code" "claude mcp add --transport http --scope user $NAME $URL"; fi rmdir "$probe" else manual "Claude Code" "claude mcp add --transport http --scope user $NAME $URL"; fi fi if have codex; then if existing=$(codex mcp get "$NAME" /dev/null); then if printf '%s\n' "$existing" | grep -Fqx " url: $URL"; then already "Codex" else conflict "Codex" "codex mcp get $NAME"; fi elif codex mcp add "$NAME" --url "$URL" /dev/null 2>&1; then added "Codex" else manual "Codex" "codex mcp add $NAME --url $URL"; fi fi if have gemini; then json_config inspect "$HOME/.gemini/settings.json" mcpServers httpUrl "{\"httpUrl\":\"$URL\"}" result=$? case "$result" in 0) if gemini mcp add --scope user --transport http "$NAME" "$URL" /dev/null 2>&1; then added "Gemini CLI" else manual "Gemini CLI" "gemini mcp add --scope user --transport http $NAME $URL"; fi ;; 10) already "Gemini CLI" ;; 11) conflict "Gemini CLI" "$HOME/.gemini/settings.json" ;; *) manual "Gemini CLI" "Check $HOME/.gemini/settings.json before adding $URL" ;; esac else config_file "Gemini CLI" "$HOME/.gemini" settings.json httpUrl "{\"httpUrl\":\"$URL\"}" mcpServers fi # VS Code keeps it in the default user profile. Inspect before its replacement-style add command. case "$(uname -s)" in Darwin) code_settings="$HOME/Library/Application Support/Code/User" ;; *) code_settings="${XDG_CONFIG_HOME:-$HOME/.config}/Code/User" ;; esac if [ -n "${VSCODE_PORTABLE:-}" ]; then code_settings="$VSCODE_PORTABLE/user-data/User"; fi if have code; then json_config inspect "$code_settings/mcp.json" servers url "{\"type\":\"http\",\"url\":\"$URL\"}" result=$? case "$result" in 0) if code --add-mcp "$JSON" /dev/null 2>&1; then added "VS Code" else manual "VS Code" "code --add-mcp '$JSON'"; fi ;; 10) already "VS Code" ;; 11) conflict "VS Code" "$code_settings/mcp.json" ;; *) manual "VS Code" "Check $code_settings/mcp.json before adding $URL" ;; esac else config_file "VS Code" "$code_settings" mcp.json url "{\"type\":\"http\",\"url\":\"$URL\"}" servers fi # Cursor and Windsurf read a settings file (Cursor's --add-mcp starts the app instead of # returning). Merge only the new server; keep all other settings and server options. if have cursor && [ ! -d "$HOME/.cursor" ]; then mkdir -p "$HOME/.cursor" || failed=$((failed + 1)) fi if have windsurf && [ ! -d "$HOME/.codeium/windsurf" ]; then mkdir -p "$HOME/.codeium/windsurf" || failed=$((failed + 1)) fi config_file "Cursor" "$HOME/.cursor" mcp.json url "{\"url\":\"$URL\"}" mcpServers config_file "Windsurf" "$HOME/.codeium/windsurf" mcp_config.json serverUrl "{\"serverUrl\":\"$URL\"}" mcpServers if [ "$found" -eq 0 ] && [ "$failed" -eq 0 ]; then echo " No AI agent command found on this computer." fi cat < Connectors > Add custom connector, paste $URL ChatGPT (eligible accounts, web): Settings > Security and login > Developer mode, then create a developer-mode app at https://chatgpt.com/plugins with $URL (No Authentication). EOF # With a person watching, put the URL on the clipboard, ready to paste. if [ -t 1 ]; then if have pbcopy; then printf %s "$URL" | pbcopy && echo " (The URL is on your clipboard.)" elif have wl-copy; then printf %s "$URL" | wl-copy && echo " (The URL is on your clipboard.)" elif have xclip; then printf %s "$URL" | xclip -selection clipboard && echo " (The URL is on your clipboard.)" fi fi echo echo "Restart your AI app, then ask: \"Semma bore ah iruku, oru gripping Tamil padam sollu?\"" [ "$failed" -eq 0 ]