cc-agy 0.0.0-stage → 1.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.
Files changed (52) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +157 -2
  3. package/assets/kit/apple-touch-icon.png +0 -0
  4. package/assets/kit/cc-agy-app-icon-128.png +0 -0
  5. package/assets/kit/cc-agy-app-icon-32.png +0 -0
  6. package/assets/kit/cc-agy-app-icon-512.png +0 -0
  7. package/assets/kit/cc-agy-app-icon-64.png +0 -0
  8. package/assets/kit/cc-agy-app-icon.svg +45 -0
  9. package/assets/kit/cc-agy-black-128.png +0 -0
  10. package/assets/kit/cc-agy-black-32.png +0 -0
  11. package/assets/kit/cc-agy-black-512.png +0 -0
  12. package/assets/kit/cc-agy-black-64.png +0 -0
  13. package/assets/kit/cc-agy-black.svg +11 -0
  14. package/assets/kit/cc-agy-favicon-128.png +0 -0
  15. package/assets/kit/cc-agy-favicon-32.png +0 -0
  16. package/assets/kit/cc-agy-favicon-512.png +0 -0
  17. package/assets/kit/cc-agy-favicon-64.png +0 -0
  18. package/assets/kit/cc-agy-favicon.svg +45 -0
  19. package/assets/kit/cc-agy-mono-38bdf8-128.png +0 -0
  20. package/assets/kit/cc-agy-mono-38bdf8-32.png +0 -0
  21. package/assets/kit/cc-agy-mono-38bdf8-512.png +0 -0
  22. package/assets/kit/cc-agy-mono-38bdf8-64.png +0 -0
  23. package/assets/kit/cc-agy-mono-38bdf8.svg +11 -0
  24. package/assets/kit/cc-agy-square-128.png +0 -0
  25. package/assets/kit/cc-agy-square-32.png +0 -0
  26. package/assets/kit/cc-agy-square-512.png +0 -0
  27. package/assets/kit/cc-agy-square-64.png +0 -0
  28. package/assets/kit/cc-agy-square.svg +45 -0
  29. package/assets/kit/cc-agy-white-128.png +0 -0
  30. package/assets/kit/cc-agy-white-32.png +0 -0
  31. package/assets/kit/cc-agy-white-512.png +0 -0
  32. package/assets/kit/cc-agy-white-64.png +0 -0
  33. package/assets/kit/cc-agy-white.svg +11 -0
  34. package/assets/kit/favicon-16.png +0 -0
  35. package/assets/kit/favicon-32.png +0 -0
  36. package/assets/kit/favicon-48.png +0 -0
  37. package/assets/kit/favicon.ico +0 -0
  38. package/assets/kit/favicon.svg +45 -0
  39. package/assets/kit/head-snippet.html +5 -0
  40. package/assets/kit/icon-192.png +0 -0
  41. package/assets/kit/icon-512.png +0 -0
  42. package/assets/kit/maskable-512.png +0 -0
  43. package/assets/kit/site.webmanifest +24 -0
  44. package/assets/logo.png +0 -0
  45. package/assets/logo.svg +45 -0
  46. package/bin/cc-agy.js +510 -0
  47. package/install.sh +212 -0
  48. package/package.json +48 -4
  49. package/scripts/import-agy.js +141 -0
  50. package/scripts/patch-combo.py +152 -0
  51. package/systemd/omniroute.service +15 -0
  52. package/uninstall.sh +64 -0
package/install.sh ADDED
@@ -0,0 +1,212 @@
1
+ #!/usr/bin/env bash
2
+ # ==============================================================================
3
+ # cc-agy: Automated Claude Code + Antigravity (Gemini 3.8 Flash High) Bootstrap
4
+ # Production-grade setup script with zero hardcoded credentials.
5
+ # ==============================================================================
6
+ set -euo pipefail
7
+
8
+ BOLD='\033[1m'
9
+ GREEN='\033[0;32m'
10
+ BLUE='\033[0;34m'
11
+ YELLOW='\033[1;33m'
12
+ RED='\033[0;31m'
13
+ NC='\033[0m'
14
+
15
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-.}")" 2>/dev/null && pwd || echo "")"
16
+ if [ -z "${SCRIPT_DIR}" ] || [ ! -f "${SCRIPT_DIR}/scripts/import-agy.js" ]; then
17
+ INSTALL_CACHE="${HOME}/.local/share/cc-agy"
18
+ mkdir -p "${HOME}/.local/share"
19
+ if [ ! -d "${INSTALL_CACHE}/.git" ]; then
20
+ echo -e "${YELLOW}[*] Bootstrapping cc-agy asset cache into ~/.local/share/cc-agy...${NC}"
21
+ git clone --depth 1 https://github.com/farhatkoka/cc-agy.git "${INSTALL_CACHE}"
22
+ else
23
+ git -C "${INSTALL_CACHE}" pull --ff-only 2>/dev/null || true
24
+ fi
25
+ SCRIPT_DIR="${INSTALL_CACHE}"
26
+ fi
27
+
28
+ echo -e "${BOLD}${BLUE}====================================================================${NC}"
29
+ echo -e "${BOLD}${BLUE} cc-agy: Claude Code + Antigravity TPU Inference Gateway ${NC}"
30
+ echo -e "${BOLD}${BLUE}====================================================================${NC}\n"
31
+
32
+ # 1. Dependency checks
33
+ echo -e "${YELLOW}[1/6] Validating host dependencies...${NC}"
34
+ command -v git >/dev/null 2>&1 || { echo -e "${RED}[-] git is required but not installed.${NC}"; exit 1; }
35
+ command -v node >/dev/null 2>&1 || { echo -e "${RED}[-] Node.js is required but not installed.${NC}"; exit 1; }
36
+ command -v npm >/dev/null 2>&1 || { echo -e "${RED}[-] npm is required but not installed.${NC}"; exit 1; }
37
+ command -v python3 >/dev/null 2>&1 || { echo -e "${RED}[-] python3 is required but not installed.${NC}"; exit 1; }
38
+ command -v systemctl >/dev/null 2>&1 || { echo -e "${RED}[-] systemd is required for daemon management.${NC}"; exit 1; }
39
+
40
+ # Check for Antigravity token
41
+ AGY_TOKEN_PATH="${HOME}/.gemini/antigravity-cli/antigravity-oauth-token"
42
+ if [ ! -f "${AGY_TOKEN_PATH}" ]; then
43
+ echo -e "${RED}[-] Antigravity OAuth token not found at: ${AGY_TOKEN_PATH}${NC}"
44
+ echo -e " Please run 'agy' once to authenticate with your Google account first."
45
+ exit 1
46
+ fi
47
+ echo -e "${GREEN}[+] Host dependencies and Antigravity OAuth token confirmed.${NC}"
48
+
49
+ # 2. Claude Code & Native Binary Installation
50
+ echo -e "\n${YELLOW}[2/6] Verifying Claude Code and native platform binaries...${NC}"
51
+ if ! command -v claude >/dev/null 2>&1; then
52
+ echo -e " Installing @anthropic-ai/claude-code globally..."
53
+ npm install -g @anthropic-ai/claude-code
54
+ fi
55
+
56
+ # Ensure platform-native binary exists to prevent 'native binary not installed' runtime error
57
+ ARCH="$(uname -m)"
58
+ OS="$(uname -s | tr '[:upper:]' '[:lower:]')"
59
+ if [ "${OS}" = "linux" ] && [ "${ARCH}" = "x86_64" ]; then
60
+ NODE_GLOBAL_ROOT="$(npm root -g)"
61
+ if [ ! -d "${NODE_GLOBAL_ROOT}/@anthropic-ai/claude-code-linux-x64" ]; then
62
+ echo -e " Installing native binary package: @anthropic-ai/claude-code-linux-x64..."
63
+ npm install -g @anthropic-ai/claude-code-linux-x64@latest || true
64
+ fi
65
+ if [ -f "${NODE_GLOBAL_ROOT}/@anthropic-ai/claude-code/install.cjs" ]; then
66
+ node "${NODE_GLOBAL_ROOT}/@anthropic-ai/claude-code/install.cjs" 2>/dev/null || true
67
+ fi
68
+ fi
69
+ echo -e "${GREEN}[+] Claude Code binary confirmed.${NC}"
70
+
71
+ # 3. OmniRoute Installation & Initialization
72
+ echo -e "\n${YELLOW}[3/6] Setting up OmniRoute local reverse proxy...${NC}"
73
+ if ! command -v omniroute >/dev/null 2>&1; then
74
+ echo -e " Installing omniroute globally..."
75
+ npm install -g omniroute
76
+ fi
77
+
78
+ mkdir -p "${HOME}/.omniroute"
79
+ OMNI_ENV="${HOME}/.omniroute/.env"
80
+ if [ ! -f "${OMNI_ENV}" ]; then
81
+ echo -e " Generating local storage encryption key..."
82
+ STORAGE_KEY="$(python3 -c "import secrets; print(secrets.token_hex(32))")"
83
+ cat << EOF > "${OMNI_ENV}"
84
+ STORAGE_ENCRYPTION_KEY="${STORAGE_KEY}"
85
+ OMNIROUTE_SERVER_HOST="127.0.0.1"
86
+ PORT=20128
87
+ EOF
88
+ fi
89
+
90
+ # Ensure OmniRoute has initialized its database
91
+ if [ ! -f "${HOME}/.omniroute/storage.sqlite" ]; then
92
+ echo -e " Initializing OmniRoute database..."
93
+ omniroute status >/dev/null 2>&1 || true
94
+ fi
95
+
96
+ # 4. Provision Antigravity Provider & Patch Combos
97
+ echo -e "\n${YELLOW}[4/6] Encrypting credentials & patching SQLite combo cascades...${NC}"
98
+ node "${SCRIPT_DIR}/scripts/import-agy.js"
99
+ python3 "${SCRIPT_DIR}/scripts/patch-combo.py"
100
+
101
+ # Resolve local API key from storage.sqlite
102
+ LOCAL_API_KEY="$(python3 -c '
103
+ import sqlite3, sys
104
+ from pathlib import Path
105
+ db = Path.home() / ".omniroute" / "storage.sqlite"
106
+ if not db.exists(): sys.exit(1)
107
+ conn = sqlite3.connect(db)
108
+ cur = conn.cursor()
109
+ row = cur.execute("SELECT key FROM api_keys LIMIT 1").fetchone()
110
+ if row:
111
+ print(row[0])
112
+ else:
113
+ import secrets
114
+ new_key = f"sk-{secrets.token_hex(8)}-{secrets.token_hex(3)}-{secrets.token_hex(4)}"
115
+ cur.execute("INSERT INTO api_keys (key, name, scopes, allowed_combos, is_active, created_at, updated_at) VALUES (?, ?, ?, ?, 1, datetime(\"now\"), datetime(\"now\"))",
116
+ (new_key, "Default Key", "[\"admin\",\"manage\",\"read\",\"write\"]", "[\"*\"]"))
117
+ conn.commit()
118
+ print(new_key)
119
+ conn.close()
120
+ ')"
121
+
122
+ if [ -z "${LOCAL_API_KEY}" ]; then
123
+ echo -e "${RED}[-] Failed to resolve or generate a local OmniRoute API key.${NC}"
124
+ exit 1
125
+ fi
126
+ echo -e "${GREEN}[+] Local API key provisioned and authorized.${NC}"
127
+
128
+ # 5. Deploy Resilient Systemd Daemon
129
+ echo -e "\n${YELLOW}[5/6] Deploying resilient systemd user service...${NC}"
130
+ SYSTEMD_USER_DIR="${HOME}/.config/systemd/user"
131
+ mkdir -p "${SYSTEMD_USER_DIR}"
132
+ cp "${SCRIPT_DIR}/systemd/omniroute.service" "${SYSTEMD_USER_DIR}/omniroute.service"
133
+
134
+ systemctl --user daemon-reload
135
+ systemctl --user enable omniroute
136
+ systemctl --user restart omniroute
137
+
138
+ # Wait for service health check
139
+ echo -n " Awaiting gateway readiness on http://127.0.0.1:20128"
140
+ for _ in {1..20}; do
141
+ if curl -s http://127.0.0.1:20128/api/monitoring/health | grep -q '"status":"healthy"' 2>/dev/null; then
142
+ echo -e " ${GREEN}[READY]${NC}"
143
+ break
144
+ fi
145
+ echo -n "."
146
+ sleep 0.5
147
+ done
148
+
149
+ # 6. Configure Claude Code Client & Shell Environment
150
+ echo -e "\n${YELLOW}[6/6] Syncing Claude Code configuration...${NC}"
151
+ mkdir -p "${HOME}/.claude"
152
+ SETTINGS_FILE="${HOME}/.claude/settings.json"
153
+
154
+ python3 -c '
155
+ import json, sys
156
+ from pathlib import Path
157
+
158
+ settings_path = Path.home() / ".claude" / "settings.json"
159
+ key = sys.argv[1]
160
+
161
+ data = {}
162
+ if settings_path.exists():
163
+ try:
164
+ with open(settings_path, "r", encoding="utf-8") as f:
165
+ data = json.load(f)
166
+ except Exception:
167
+ data = {}
168
+
169
+ if "env" not in data or not isinstance(data["env"], dict):
170
+ data["env"] = {}
171
+
172
+ data["$schema"] = "https://json.schemastore.org/claude-code-settings.json"
173
+ data["env"]["ANTHROPIC_BASE_URL"] = "http://localhost:20128"
174
+ data["env"]["ANTHROPIC_AUTH_TOKEN"] = key
175
+ data["env"]["ANTHROPIC_API_KEY"] = ""
176
+ data["env"]["CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY"] = "1"
177
+ data["env"]["CLAUDE_CODE_AUTO_COMPACT_WINDOW"] = "190000"
178
+
179
+ with open(settings_path, "w", encoding="utf-8") as f:
180
+ json.dump(data, f, indent=2)
181
+ ' "${LOCAL_API_KEY}"
182
+
183
+ # Install self-healing cc-agy CLI executable
184
+ mkdir -p "${HOME}/.local/bin"
185
+ cp "${SCRIPT_DIR}/bin/cc-agy.js" "${HOME}/.local/bin/cc-agy"
186
+ chmod +x "${HOME}/.local/bin/cc-agy"
187
+ echo -e "${GREEN}[+] Self-healing 'cc-agy' CLI installed to ~/.local/bin/cc-agy${NC}"
188
+
189
+ # Append idempotent block to ~/.bashrc if not present
190
+ BASHRC="${HOME}/.bashrc"
191
+ if [ -f "${BASHRC}" ]; then
192
+ if ! grep -q "CC-AGY GATEWAY" "${BASHRC}"; then
193
+ cat << EOF >> "${BASHRC}"
194
+
195
+ # CC-AGY GATEWAY (Claude Code + Antigravity)
196
+ export OMNIROUTE_API_KEY="${LOCAL_API_KEY}"
197
+ export ANTHROPIC_BASE_URL="http://localhost:20128"
198
+ export ANTHROPIC_AUTH_TOKEN="${LOCAL_API_KEY}"
199
+ export ANTHROPIC_API_KEY=""
200
+ EOF
201
+ fi
202
+ fi
203
+
204
+ echo -e "\n${BOLD}${GREEN}====================================================================${NC}"
205
+ echo -e "${BOLD}${GREEN} SUCCESS: cc-agy Pipeline Fully Operational! ${NC}"
206
+ echo -e "${BOLD}${GREEN}====================================================================${NC}"
207
+ echo -e "Primary Model : ${BOLD}Gemini 3.8 Flash (High Reasoning)${NC}"
208
+ echo -e "Fallback Models: ${BOLD}Claude Sonnet 4.6 -> Gemini 3.1 Flash Lite${NC}"
209
+ echo -e "Daemon Service : ${BOLD}systemctl --user status omniroute${NC}"
210
+ echo -e "Endpoint : ${BOLD}http://localhost:20128/v1${NC}"
211
+ echo -e "CLI Command : ${BOLD}cc-agy${NC} (or 'claude')"
212
+ echo -e "\nRun ${BOLD}'cc-agy'${NC} anywhere in your terminal to begin coding."
package/package.json CHANGED
@@ -1,6 +1,50 @@
1
1
  {
2
2
  "name": "cc-agy",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.1.0",
4
+ "description": "Decoupled Asymmetric Compute Gateway: Claude Code CLI + Google Antigravity (Gemini 3.8 Flash High) TPU compute at zero marginal token cost.",
5
+ "main": "bin/cc-agy.js",
6
+ "bin": {
7
+ "cc-agy": "bin/cc-agy.js"
8
+ },
9
+ "scripts": {
10
+ "start": "node ./bin/cc-agy.js",
11
+ "doctor": "node ./bin/cc-agy.js doctor",
12
+ "status": "node ./bin/cc-agy.js status",
13
+ "restart": "node ./bin/cc-agy.js restart",
14
+ "setup": "bash ./install.sh"
15
+ },
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/farhatkoka/cc-agy.git"
19
+ },
20
+ "keywords": [
21
+ "claude",
22
+ "claude-code",
23
+ "antigravity",
24
+ "gemini",
25
+ "omniroute",
26
+ "ai",
27
+ "cli",
28
+ "tpu",
29
+ "gateway"
30
+ ],
31
+ "author": "Farhat Iqbal <https://github.com/farhatkoka>",
32
+ "license": "MIT",
33
+ "bugs": {
34
+ "url": "https://github.com/farhatkoka/cc-agy/issues"
35
+ },
36
+ "homepage": "https://github.com/farhatkoka/cc-agy#readme",
37
+ "files": [
38
+ "bin",
39
+ "scripts",
40
+ "systemd",
41
+ "assets",
42
+ "install.sh",
43
+ "uninstall.sh",
44
+ "README.md",
45
+ "LICENSE"
46
+ ],
47
+ "engines": {
48
+ "node": ">=18.0.0"
49
+ }
50
+ }
@@ -0,0 +1,141 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * import-agy.js
4
+ * Provisions Antigravity credentials into OmniRoute SQLite using AES-256-GCM.
5
+ * Zero external pip/npm dependencies (pure Node.js standard library).
6
+ */
7
+
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+ const os = require('os');
11
+ const crypto = require('crypto');
12
+ const { spawnSync } = require('child_process');
13
+
14
+ function getEnvValue(envPath, key) {
15
+ if (!fs.existsSync(envPath)) return null;
16
+ const content = fs.readFileSync(envPath, 'utf8');
17
+ for (const line of content.split('\n')) {
18
+ const trimmed = line.trim();
19
+ if (trimmed.startsWith(`${key}=`)) {
20
+ let val = trimmed.slice(key.length + 1).trim();
21
+ if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
22
+ val = val.slice(1, -1);
23
+ }
24
+ return val;
25
+ }
26
+ }
27
+ return null;
28
+ }
29
+
30
+ function encrypt(text, staticKey) {
31
+ if (!text) return null;
32
+ const iv = crypto.randomBytes(16);
33
+ const cipher = crypto.createCipheriv('aes-256-gcm', staticKey, iv);
34
+ let enc = cipher.update(text, 'utf8', 'hex');
35
+ enc += cipher.final('hex');
36
+ const tag = cipher.getAuthTag().toString('hex');
37
+ return `enc:v1:${iv.toString('hex')}:${enc}:${tag}`;
38
+ }
39
+
40
+ function main() {
41
+ const home = os.homedir();
42
+ const tokenFile = path.join(home, '.gemini', 'antigravity-cli', 'antigravity-oauth-token');
43
+ if (!fs.existsSync(tokenFile)) {
44
+ console.error(`[-] Antigravity token not found at: ${tokenFile}`);
45
+ console.error(` Run 'agy' once to sign in with your Google account first.`);
46
+ process.exit(1);
47
+ }
48
+
49
+ const tokenData = JSON.parse(fs.readFileSync(tokenFile, 'utf8'));
50
+ const tokenObj = tokenData.token || tokenData;
51
+ const accessToken = tokenObj.access_token;
52
+ const refreshToken = tokenObj.refresh_token;
53
+
54
+ if (!accessToken || !refreshToken) {
55
+ console.error(`[-] Missing access_token or refresh_token in token file.`);
56
+ process.exit(1);
57
+ }
58
+
59
+ const omniEnv = path.join(home, '.omniroute', '.env');
60
+ const encKey = getEnvValue(omniEnv, 'STORAGE_ENCRYPTION_KEY');
61
+ if (!encKey) {
62
+ console.error(`[-] STORAGE_ENCRYPTION_KEY not found in ${omniEnv}`);
63
+ process.exit(1);
64
+ }
65
+
66
+ const staticKey = crypto.scryptSync(encKey, 'omniroute-field-encryption-v1', 32);
67
+ const encAccessToken = encrypt(accessToken, staticKey);
68
+ const encRefreshToken = encrypt(refreshToken, staticKey);
69
+
70
+ const email = tokenData.id_token_email || '';
71
+ const projectId = 'aicode-consumers';
72
+ const expiresAt = tokenObj.expiry || tokenObj.expires_at || new Date(Date.now() + 3600000).toISOString();
73
+
74
+ const providerSpecificData = JSON.stringify({
75
+ clientProfile: 'cli',
76
+ tokenType: 'Bearer',
77
+ authMethod: 'consumer',
78
+ importedAt: new Date().toISOString(),
79
+ projectId,
80
+ tier: 'g1-pro-tier',
81
+ subscriptionTier: 'Google AI Pro',
82
+ plan: 'Pro'
83
+ });
84
+
85
+ const dbPath = path.join(home, '.omniroute', 'storage.sqlite');
86
+ if (!fs.existsSync(dbPath)) {
87
+ console.error(`[-] OmniRoute database not found at ${dbPath}`);
88
+ process.exit(1);
89
+ }
90
+
91
+ // Use python3 sqlite3 to run parametrized SQL safely
92
+ const pythonScript = `
93
+ import sys, sqlite3, uuid
94
+
95
+ db_path, access_token, refresh_token, email, project_id, expires_at, spec_data = sys.argv[1:8]
96
+ conn = sqlite3.connect(db_path)
97
+ cur = conn.cursor()
98
+
99
+ row = cur.execute("SELECT id FROM provider_connections WHERE provider = 'agy'").fetchone()
100
+ if row:
101
+ conn_id = row[0]
102
+ cur.execute("""
103
+ UPDATE provider_connections
104
+ SET access_token = ?, refresh_token = ?, email = ?, project_id = ?,
105
+ expires_at = ?, provider_specific_data = ?, is_active = 1, updated_at = datetime('now')
106
+ WHERE id = ?
107
+ """, (access_token, refresh_token, email, project_id, expires_at, spec_data, conn_id))
108
+ print(f"[+] Synchronized existing 'agy' credentials ({conn_id[:8]}...)")
109
+ else:
110
+ conn_id = str(uuid.uuid4())
111
+ cur.execute("""
112
+ INSERT INTO provider_connections
113
+ (id, provider, name, auth_type, access_token, refresh_token, email, project_id, expires_at, provider_specific_data, is_active, created_at, updated_at)
114
+ VALUES (?, 'agy', 'Antigravity CLI (Imported)', 'oauth', ?, ?, ?, ?, ?, ?, 1, datetime('now'), datetime('now'))
115
+ """, (conn_id, access_token, refresh_token, email, project_id, expires_at, spec_data))
116
+ print(f"[+] Created 'agy' provider connection ({conn_id[:8]}...)")
117
+
118
+ conn.commit()
119
+ conn.close()
120
+ `;
121
+
122
+ const res = spawnSync('python3', [
123
+ '-c', pythonScript,
124
+ dbPath,
125
+ encAccessToken,
126
+ encRefreshToken,
127
+ email,
128
+ projectId,
129
+ expiresAt,
130
+ providerSpecificData
131
+ ], { encoding: 'utf-8' });
132
+
133
+ if (res.status !== 0) {
134
+ console.error(`[-] SQLite update failed:`, res.stderr);
135
+ process.exit(1);
136
+ } else {
137
+ process.stdout.write(res.stdout);
138
+ }
139
+ }
140
+
141
+ main();
@@ -0,0 +1,152 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ patch-combo.py
4
+ Configures OmniRoute SQLite database for Claude Code + Antigravity compatibility:
5
+ 1. Provisions exact Google Antigravity models and aliases (Gemini 3.8 Flash High/Med/Low, Claude Sonnet 4.6, Claude Opus 4.6, GPT-OSS 120B).
6
+ 2. Enables compatFilterFailOpen on combos to prevent fatal 400 capability errors.
7
+ 3. Sets up fallback 'claude-auto' priority cascade.
8
+ 4. Ensures local API keys have wildcard permissions for combo routing.
9
+ """
10
+
11
+ import sys
12
+ import json
13
+ import sqlite3
14
+ from pathlib import Path
15
+
16
+ MODELS_CONFIG = [
17
+ {
18
+ 'id': 'combo-gemini-3.8-high',
19
+ 'name': 'gemini-3.8-flash-high',
20
+ 'aliases': ['flash-high', 'flash', 'high', 'gemini'],
21
+ 'target': 'agy/gemini-3.8-flash-high'
22
+ },
23
+ {
24
+ 'id': 'combo-gemini-3.8-med',
25
+ 'name': 'gemini-3.8-flash-medium',
26
+ 'aliases': ['flash-medium', 'medium', 'med'],
27
+ 'target': 'agy/gemini-3.8-flash-medium'
28
+ },
29
+ {
30
+ 'id': 'combo-gemini-3.8-low',
31
+ 'name': 'gemini-3.8-flash-low',
32
+ 'aliases': ['flash-low', 'low'],
33
+ 'target': 'agy/gemini-3.8-flash-low'
34
+ },
35
+ {
36
+ 'id': 'combo-claude-sonnet-4-6',
37
+ 'name': 'claude-sonnet-4-6',
38
+ 'aliases': ['sonnet', 'sonnet-4-6'],
39
+ 'target': 'agy/claude-sonnet-4-6'
40
+ },
41
+ {
42
+ 'id': 'combo-claude-opus-4-6',
43
+ 'name': 'claude-opus-4-6-thinking',
44
+ 'aliases': ['opus', 'opus-4-6', 'opus-thinking'],
45
+ 'target': 'agy/claude-opus-4-6-thinking'
46
+ },
47
+ {
48
+ 'id': 'combo-gpt-oss-120b',
49
+ 'name': 'gpt-oss-120b-medium',
50
+ 'aliases': ['gpt', 'oss', 'gpt-oss'],
51
+ 'target': 'agy/gpt-oss-120b-medium'
52
+ }
53
+ ]
54
+
55
+ def patch_omniroute_db():
56
+ db_path = Path.home() / ".omniroute" / "storage.sqlite"
57
+ if not db_path.exists():
58
+ print(f"[-] Database not found at {db_path}. Start OmniRoute at least once before patching.", file=sys.stderr)
59
+ return False
60
+
61
+ conn = sqlite3.connect(db_path)
62
+ cur = conn.cursor()
63
+
64
+ try:
65
+ # 1. Setup exact model combos and alias mappings
66
+ for m in MODELS_CONFIG:
67
+ combo_data = {
68
+ 'name': m['name'],
69
+ 'strategy': 'priority',
70
+ 'config': {'compatFilterFailOpen': True},
71
+ 'models': [{'id': m['id'] + '-m', 'kind': 'model', 'model': m['target'], 'providerId': 'agy', 'weight': 10}],
72
+ 'version': 2
73
+ }
74
+ cur.execute("""INSERT OR REPLACE INTO combos (id, name, data, updated_at, created_at)
75
+ VALUES (?, ?, ?, datetime('now'), datetime('now'))""",
76
+ (m['id'], m['name'], json.dumps(combo_data)))
77
+
78
+ cur.execute("""INSERT OR REPLACE INTO model_combo_mappings (id, pattern, combo_id, priority, enabled, description, created_at, updated_at)
79
+ VALUES (?, ?, ?, 20, 1, ?, datetime('now'), datetime('now'))""",
80
+ ('map-' + m['name'], m['name'], m['id'], 'Exact ' + m['name']))
81
+
82
+ for alias in m['aliases']:
83
+ cur.execute("""INSERT OR REPLACE INTO model_combo_mappings (id, pattern, combo_id, priority, enabled, description, created_at, updated_at)
84
+ VALUES (?, ?, ?, 15, 1, ?, datetime('now'), datetime('now'))""",
85
+ ('map-alias-' + alias, alias, m['id'], 'Alias ' + alias))
86
+
87
+ # 2. Patch claude-auto priority fallback combo
88
+ row = cur.execute("SELECT id, data FROM combos WHERE name = 'claude-auto'").fetchone()
89
+ auto_id = "fc0a1ddb-af40-4c9d-b9ce-7e8166c3ddde"
90
+ if not row:
91
+ combo_data = {
92
+ "name": "claude-auto",
93
+ "strategy": "priority",
94
+ "config": {"compatFilterFailOpen": True},
95
+ "models": [
96
+ {"id": "claude-auto-m1", "kind": "model", "model": "agy/gemini-3.8-flash-high", "providerId": "agy", "weight": 10},
97
+ {"id": "claude-auto-m2", "kind": "model", "model": "agy/claude-sonnet-4-6", "providerId": "agy", "weight": 5},
98
+ {"id": "claude-auto-m3", "kind": "model", "model": "agy/gemini-3.1-flash-lite", "providerId": "agy", "weight": 1}
99
+ ],
100
+ "version": 2
101
+ }
102
+ cur.execute(
103
+ "INSERT INTO combos (id, name, data, sort_order, created_at, updated_at) VALUES (?, ?, ?, 1, datetime('now'), datetime('now'))",
104
+ (auto_id, "claude-auto", json.dumps(combo_data))
105
+ )
106
+ else:
107
+ auto_id, raw_data = row
108
+ data = json.loads(raw_data)
109
+ if "config" not in data or not isinstance(data["config"], dict):
110
+ data["config"] = {}
111
+ data["config"]["compatFilterFailOpen"] = True
112
+ data["strategy"] = "priority"
113
+ data["models"] = [
114
+ {"id": "claude-auto-m1", "kind": "model", "model": "agy/gemini-3.8-flash-high", "providerId": "agy", "weight": 10},
115
+ {"id": "claude-auto-m2", "kind": "model", "model": "agy/claude-sonnet-4-6", "providerId": "agy", "weight": 5},
116
+ {"id": "claude-auto-m3", "kind": "model", "model": "agy/gemini-3.1-flash-lite", "providerId": "agy", "weight": 1}
117
+ ]
118
+ cur.execute("UPDATE combos SET data = ?, updated_at = datetime('now') WHERE name = 'claude-auto'", (json.dumps(data),))
119
+
120
+ # Fallback mappings for generic legacy requests
121
+ cur.execute("""INSERT OR REPLACE INTO model_combo_mappings (id, pattern, combo_id, priority, enabled, description, created_at, updated_at)
122
+ VALUES ('map-claude-fallback-sonnet', 'claude-3-5-sonnet*', ?, 5, 1, 'Default Sonnet to Auto', datetime('now'), datetime('now'))""", (auto_id,))
123
+ cur.execute("""INSERT OR REPLACE INTO model_combo_mappings (id, pattern, combo_id, priority, enabled, description, created_at, updated_at)
124
+ VALUES ('map-claude-fallback-opus', 'claude-opus-5*', ?, 5, 1, 'Default Opus to Auto', datetime('now'), datetime('now'))""", (auto_id,))
125
+
126
+ # 3. Grant wildcard combo permissions to all local API keys
127
+ all_combos = [m['name'] for m in MODELS_CONFIG] + [a for m in MODELS_CONFIG for a in m['aliases']] + ['claude-auto', '*', 'combo/*', 'auto/*']
128
+ keys = cur.execute("SELECT key FROM api_keys").fetchall()
129
+ for (key_val,) in keys:
130
+ cur.execute(
131
+ "UPDATE api_keys SET allowed_combos = ?, scopes = ? WHERE key = ?",
132
+ (json.dumps(all_combos), json.dumps(["admin", "manage", "read", "write"]), key_val)
133
+ )
134
+
135
+ conn.commit()
136
+ print("[+] OmniRoute SQLite database successfully patched:")
137
+ print(" - 6 exact Antigravity models and aliases mapped")
138
+ print(" - 'claude-auto' cascade configured (Gemini 3.8 Flash High -> Sonnet 4.6 -> Flash Lite)")
139
+ print(" - 'compatFilterFailOpen' enabled across all combos")
140
+ print(" - Local API key permissions synchronized")
141
+ return True
142
+
143
+ except Exception as e:
144
+ conn.rollback()
145
+ print(f"[-] Database patch error: {e}", file=sys.stderr)
146
+ return False
147
+ finally:
148
+ conn.close()
149
+
150
+ if __name__ == "__main__":
151
+ if not patch_omniroute_db():
152
+ sys.exit(1)
@@ -0,0 +1,15 @@
1
+ [Unit]
2
+ Description=OmniRoute AI proxy router (User Gateway)
3
+ After=network-online.target
4
+ Wants=network-online.target
5
+
6
+ [Service]
7
+ Type=simple
8
+ ExecStart=/usr/bin/node %h/.local/lib/node_modules/omniroute/bin/omniroute.mjs serve --no-open
9
+ Restart=always
10
+ RestartSec=3
11
+ Environment="PATH=%h/.local/bin:/usr/local/bin:/usr/bin:/bin"
12
+ EnvironmentFile=-%h/.omniroute/.env
13
+
14
+ [Install]
15
+ WantedBy=default.target
package/uninstall.sh ADDED
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env bash
2
+ # ==============================================================================
3
+ # cc-agy: Uninstaller
4
+ # Restores original Claude Code settings, stops systemd service, and cleans up.
5
+ # ==============================================================================
6
+ set -euo pipefail
7
+
8
+ BOLD='\033[1m'
9
+ GREEN='\033[0;32m'
10
+ YELLOW='\033[1;33m'
11
+ RED='\033[0;31m'
12
+ NC='\033[0m'
13
+
14
+ echo -e "${BOLD}${YELLOW}Uninstalling cc-agy Gateway...${NC}"
15
+
16
+ # 1. Stop and disable systemd service
17
+ if systemctl --user is-active --quiet omniroute 2>/dev/null; then
18
+ echo -e " Stopping omniroute systemd user service..."
19
+ systemctl --user stop omniroute 2>/dev/null || true
20
+ fi
21
+
22
+ if systemctl --user is-enabled --quiet omniroute 2>/dev/null; then
23
+ echo -e " Disabling omniroute systemd user service..."
24
+ systemctl --user disable omniroute 2>/dev/null || true
25
+ fi
26
+
27
+ rm -f "${HOME}/.config/systemd/user/omniroute.service"
28
+ systemctl --user daemon-reload
29
+
30
+ # 2. Clean ~/.claude/settings.json
31
+ SETTINGS_FILE="${HOME}/.claude/settings.json"
32
+ if [ -f "${SETTINGS_FILE}" ]; then
33
+ echo -e " Restoring ~/.claude/settings.json..."
34
+ python3 -c '
35
+ import json
36
+ from pathlib import Path
37
+ p = Path.home() / ".claude" / "settings.json"
38
+ try:
39
+ with open(p, "r") as f:
40
+ data = json.load(f)
41
+ if "env" in data:
42
+ data["env"].pop("ANTHROPIC_BASE_URL", None)
43
+ data["env"].pop("ANTHROPIC_AUTH_TOKEN", None)
44
+ data["env"].pop("ANTHROPIC_API_KEY", None)
45
+ with open(p, "w") as f:
46
+ json.dump(data, f, indent=2)
47
+ except Exception:
48
+ pass
49
+ '
50
+ fi
51
+
52
+ # 3. Clean ~/.bashrc
53
+ BASHRC="${HOME}/.bashrc"
54
+ if [ -f "${BASHRC}" ]; then
55
+ echo -e " Cleaning ~/.bashrc exports..."
56
+ sed -i '/# CC-AGY GATEWAY/,+5d' "${BASHRC}" 2>/dev/null || true
57
+ fi
58
+
59
+ # 4. Remove cc-agy CLI executable and cache
60
+ echo -e " Removing ~/.local/bin/cc-agy and cache..."
61
+ rm -f "${HOME}/.local/bin/cc-agy"
62
+ rm -rf "${HOME}/.local/share/cc-agy"
63
+
64
+ echo -e "${BOLD}${GREEN}[+] cc-agy gateway successfully uninstalled.${NC}"