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.
- package/LICENSE +21 -0
- package/README.md +157 -2
- package/assets/kit/apple-touch-icon.png +0 -0
- package/assets/kit/cc-agy-app-icon-128.png +0 -0
- package/assets/kit/cc-agy-app-icon-32.png +0 -0
- package/assets/kit/cc-agy-app-icon-512.png +0 -0
- package/assets/kit/cc-agy-app-icon-64.png +0 -0
- package/assets/kit/cc-agy-app-icon.svg +45 -0
- package/assets/kit/cc-agy-black-128.png +0 -0
- package/assets/kit/cc-agy-black-32.png +0 -0
- package/assets/kit/cc-agy-black-512.png +0 -0
- package/assets/kit/cc-agy-black-64.png +0 -0
- package/assets/kit/cc-agy-black.svg +11 -0
- package/assets/kit/cc-agy-favicon-128.png +0 -0
- package/assets/kit/cc-agy-favicon-32.png +0 -0
- package/assets/kit/cc-agy-favicon-512.png +0 -0
- package/assets/kit/cc-agy-favicon-64.png +0 -0
- package/assets/kit/cc-agy-favicon.svg +45 -0
- package/assets/kit/cc-agy-mono-38bdf8-128.png +0 -0
- package/assets/kit/cc-agy-mono-38bdf8-32.png +0 -0
- package/assets/kit/cc-agy-mono-38bdf8-512.png +0 -0
- package/assets/kit/cc-agy-mono-38bdf8-64.png +0 -0
- package/assets/kit/cc-agy-mono-38bdf8.svg +11 -0
- package/assets/kit/cc-agy-square-128.png +0 -0
- package/assets/kit/cc-agy-square-32.png +0 -0
- package/assets/kit/cc-agy-square-512.png +0 -0
- package/assets/kit/cc-agy-square-64.png +0 -0
- package/assets/kit/cc-agy-square.svg +45 -0
- package/assets/kit/cc-agy-white-128.png +0 -0
- package/assets/kit/cc-agy-white-32.png +0 -0
- package/assets/kit/cc-agy-white-512.png +0 -0
- package/assets/kit/cc-agy-white-64.png +0 -0
- package/assets/kit/cc-agy-white.svg +11 -0
- package/assets/kit/favicon-16.png +0 -0
- package/assets/kit/favicon-32.png +0 -0
- package/assets/kit/favicon-48.png +0 -0
- package/assets/kit/favicon.ico +0 -0
- package/assets/kit/favicon.svg +45 -0
- package/assets/kit/head-snippet.html +5 -0
- package/assets/kit/icon-192.png +0 -0
- package/assets/kit/icon-512.png +0 -0
- package/assets/kit/maskable-512.png +0 -0
- package/assets/kit/site.webmanifest +24 -0
- package/assets/logo.png +0 -0
- package/assets/logo.svg +45 -0
- package/bin/cc-agy.js +510 -0
- package/install.sh +212 -0
- package/package.json +48 -4
- package/scripts/import-agy.js +141 -0
- package/scripts/patch-combo.py +152 -0
- package/systemd/omniroute.service +15 -0
- 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": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
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}"
|