@xkei/openclaude 0.30.0-antigravity
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 +29 -0
- package/README.md +518 -0
- package/bin/import-specifier.mjs +13 -0
- package/bin/import-specifier.test.mjs +13 -0
- package/bin/node-compile-cache.mjs +17 -0
- package/bin/openclaude +126 -0
- package/dist/cli.mjs +11292 -0
- package/dist/sdk.mjs +284293 -0
- package/docs/antigravity-plugin-install.md +223 -0
- package/docs/windows-aliases-and-launchers.md +162 -0
- package/package.json +226 -0
- package/scripts/windows/openclaude-aliases.ps1 +206 -0
- package/src/entrypoints/sdk/coreTypes.generated.ts +2385 -0
- package/src/entrypoints/sdk.d.ts +601 -0
- package/vendor/node-domexception-shim/index.js +3 -0
- package/vendor/node-domexception-shim/package.json +8 -0
- package/vendor/openclaude-antigravity-provider/.claude-plugin/marketplace.json +17 -0
- package/vendor/openclaude-antigravity-provider/.claude-plugin/plugin.json +38 -0
- package/vendor/openclaude-antigravity-provider/bin/antigravity-proxy.exe +0 -0
- package/vendor/openclaude-antigravity-provider/hooks/SessionEnd.ps1 +22 -0
- package/vendor/openclaude-antigravity-provider/hooks/SessionStart.ps1 +111 -0
- package/vendor/openclaude-antigravity-provider/hooks/Watchdog-Stop.ps1 +82 -0
- package/vendor/openclaude-antigravity-provider/hooks/hooks.json +37 -0
- package/vendor/openclaude-antigravity-provider/hooks/inject-provider.js +110 -0
- package/vendor/openclaude-antigravity-provider/hooks/session-end.bat +22 -0
- package/vendor/openclaude-antigravity-provider/hooks/start.bat +7 -0
- package/vendor/openclaude-antigravity-provider/package.json +24 -0
- package/vendor/openclaude-antigravity-provider/src/accounts.ts +115 -0
- package/vendor/openclaude-antigravity-provider/src/auth-cli.ts +125 -0
- package/vendor/openclaude-antigravity-provider/src/auth.ts +221 -0
- package/vendor/openclaude-antigravity-provider/src/config.ts +68 -0
- package/vendor/openclaude-antigravity-provider/src/constants.ts +139 -0
- package/vendor/openclaude-antigravity-provider/src/gemini-fallback.ts +157 -0
- package/vendor/openclaude-antigravity-provider/src/server.ts +367 -0
- package/vendor/openclaude-antigravity-provider/src/storage.ts +56 -0
- package/vendor/openclaude-antigravity-provider/src/transform.ts +241 -0
- package/vendor/openclaude-antigravity-provider/tsconfig.json +15 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# SessionStart.ps1
|
|
2
|
+
# OpenClaude hook: ensures the Antigravity proxy is running BEFORE OpenClaude
|
|
3
|
+
# sends its first request, and ensures OpenClaude's provider config points at
|
|
4
|
+
# the proxy (idempotent — injects only on fresh setups).
|
|
5
|
+
#
|
|
6
|
+
# Prefers the compiled exe (no bun dependency, fast start); falls back to
|
|
7
|
+
# `bun run src/server.ts` if the exe is missing.
|
|
8
|
+
#
|
|
9
|
+
# Path resolution is fully relative to this script, so the hook works from:
|
|
10
|
+
# - the npm-installed package (node_modules/@xkei/openclaude/vendor/...)
|
|
11
|
+
# - OpenClaude's versioned plugin cache (~/.openclaude/plugins/cache/...)
|
|
12
|
+
# - any source checkout
|
|
13
|
+
# Compatible with PowerShell 5.1+
|
|
14
|
+
|
|
15
|
+
$ProxyPort = 51122
|
|
16
|
+
$HealthUrl = "http://127.0.0.1:$ProxyPort/health"
|
|
17
|
+
$ProjectRoot = Split-Path -Parent $PSScriptRoot
|
|
18
|
+
$ServerScript = Join-Path $ProjectRoot "src\server.ts"
|
|
19
|
+
$ServerExe = Join-Path $ProjectRoot "bin\antigravity-proxy.exe"
|
|
20
|
+
$PidFile = Join-Path $env:USERPROFILE ".openclaude\antigravity-proxy.pid"
|
|
21
|
+
|
|
22
|
+
# ── 1. Is the proxy already healthy? ─────────────────────────────────────────
|
|
23
|
+
$healthy = $false
|
|
24
|
+
try {
|
|
25
|
+
$r = Invoke-RestMethod -Uri $HealthUrl -TimeoutSec 1 -ErrorAction Stop
|
|
26
|
+
if ($r.status -eq "ok") { $healthy = $true }
|
|
27
|
+
} catch {}
|
|
28
|
+
|
|
29
|
+
# ── 2. Not healthy → resolve launcher and spawn ──────────────────────────────
|
|
30
|
+
if (-not $healthy) {
|
|
31
|
+
$cmdLine = $null
|
|
32
|
+
if (Test-Path -LiteralPath $ServerExe) {
|
|
33
|
+
$cmdLine = "`"$ServerExe`""
|
|
34
|
+
} else {
|
|
35
|
+
# Fallback: run from source via bun (absolute path incl. Kiro-CLI fallback)
|
|
36
|
+
$bunExe = $null
|
|
37
|
+
$bunCmd = Get-Command bun -ErrorAction SilentlyContinue
|
|
38
|
+
if ($bunCmd) {
|
|
39
|
+
$bunExe = $bunCmd.Source
|
|
40
|
+
} else {
|
|
41
|
+
$fallback = Join-Path $env:LOCALAPPDATA "Kiro-Cli\bun.exe"
|
|
42
|
+
if (Test-Path -LiteralPath $fallback) { $bunExe = $fallback }
|
|
43
|
+
}
|
|
44
|
+
if ($bunExe -and (Test-Path -LiteralPath $ServerScript)) {
|
|
45
|
+
$cmdLine = "`"$bunExe`" run `"$ServerScript`""
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (-not $cmdLine) {
|
|
49
|
+
Write-Warning "[antigravity-provider] No launcher found. Build with: bun run build"
|
|
50
|
+
} else {
|
|
51
|
+
# Spawn proxy via WMI (fully detached)
|
|
52
|
+
$result = Invoke-WmiMethod -Class Win32_Process -Name Create `
|
|
53
|
+
-ArgumentList $cmdLine, $ProjectRoot
|
|
54
|
+
if ($result.ReturnValue -ne 0) {
|
|
55
|
+
Write-Warning "[antigravity-provider] Failed to spawn proxy (WMI code $($result.ReturnValue))"
|
|
56
|
+
} else {
|
|
57
|
+
$result.ProcessId | Out-File -FilePath $PidFile -Encoding utf8 -Force
|
|
58
|
+
|
|
59
|
+
# Wait until healthy so the port is OPEN before OpenClaude proceeds.
|
|
60
|
+
# Blocks briefly to prevent the ECONNREFUSED race; exits as soon as
|
|
61
|
+
# healthy (typically < 2s).
|
|
62
|
+
for ($i = 0; $i -lt 20; $i++) {
|
|
63
|
+
Start-Sleep -Milliseconds 400
|
|
64
|
+
try {
|
|
65
|
+
$check = Invoke-RestMethod -Uri $HealthUrl -TimeoutSec 1 -ErrorAction Stop
|
|
66
|
+
if ($check.status -eq "ok") { $healthy = $true; break }
|
|
67
|
+
} catch {}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if ($healthy) {
|
|
74
|
+
Write-Host "[antigravity-provider] Proxy ready -> http://127.0.0.1:$ProxyPort/v1"
|
|
75
|
+
} else {
|
|
76
|
+
Write-Warning "[antigravity-provider] Proxy not healthy yet (slow AV scan?). It may come up shortly."
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
# ── 3. Ensure OpenClaude's provider config points at the proxy ───────────────
|
|
80
|
+
# Idempotent no-op when already configured; injects on fresh setups so /model
|
|
81
|
+
# auto-discovers the Antigravity/Gemini models via the /v1/models endpoint.
|
|
82
|
+
$Injector = Join-Path $ProjectRoot "hooks\inject-provider.js"
|
|
83
|
+
if (Test-Path -LiteralPath $Injector) {
|
|
84
|
+
$jsExe = $null
|
|
85
|
+
$bunCmd2 = Get-Command bun -ErrorAction SilentlyContinue
|
|
86
|
+
if ($bunCmd2) {
|
|
87
|
+
$jsExe = $bunCmd2.Source
|
|
88
|
+
} else {
|
|
89
|
+
$k = Join-Path $env:LOCALAPPDATA "Kiro-Cli\bun.exe"
|
|
90
|
+
if (Test-Path -LiteralPath $k) { $jsExe = $k }
|
|
91
|
+
else {
|
|
92
|
+
$n = Get-Command node -ErrorAction SilentlyContinue
|
|
93
|
+
if ($n) { $jsExe = $n.Source }
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if ($jsExe) {
|
|
97
|
+
try {
|
|
98
|
+
$injectOut = & $jsExe $Injector 2>&1
|
|
99
|
+
if ($injectOut) { Write-Host "$injectOut" }
|
|
100
|
+
} catch {}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
# ── 4. Pre-warm /v1/models so model discovery is instant ─────────────────────
|
|
105
|
+
if ($healthy) {
|
|
106
|
+
try {
|
|
107
|
+
$null = Invoke-RestMethod -Uri "http://127.0.0.1:$ProxyPort/v1/models" -TimeoutSec 3 -ErrorAction Stop
|
|
108
|
+
} catch {}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
exit 0
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Watchdog-Stop.ps1
|
|
2
|
+
# Detached shutdown watchdog for the Antigravity proxy.
|
|
3
|
+
# Spawned via WMI by SessionEnd.ps1 (fully detached — survives OpenClaude's
|
|
4
|
+
# shutdown even when the SessionEnd hook itself gets cancelled during teardown).
|
|
5
|
+
#
|
|
6
|
+
# Polls up to 20 times (3s apart, ~60s total) so that even a slow OpenClaude
|
|
7
|
+
# teardown is outlived. At each poll, if NO openclaude process is still alive,
|
|
8
|
+
# the proxy is stopped. If any openclaude session survives (interactive,
|
|
9
|
+
# background, or teardown in progress), the proxy stays.
|
|
10
|
+
#
|
|
11
|
+
# Identity-verified kill: only processes named "antigravity-proxy" or "bun"
|
|
12
|
+
# are ever stopped, so nothing else can be harmed.
|
|
13
|
+
# Compatible with PowerShell 5.1+
|
|
14
|
+
|
|
15
|
+
$ProxyPort = 51122
|
|
16
|
+
$PidFile = Join-Path $env:USERPROFILE ".openclaude\antigravity-proxy.pid"
|
|
17
|
+
|
|
18
|
+
function Get-ProxyPid {
|
|
19
|
+
try {
|
|
20
|
+
if (Test-Path -LiteralPath $PidFile) {
|
|
21
|
+
return [int]((Get-Content $PidFile -Raw).Trim())
|
|
22
|
+
}
|
|
23
|
+
} catch {}
|
|
24
|
+
return 0
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function Stop-Proxy {
|
|
28
|
+
$killed = $false
|
|
29
|
+
$proxyPid = Get-ProxyPid
|
|
30
|
+
if ($proxyPid -gt 0) {
|
|
31
|
+
$p = Get-Process -Id $proxyPid -ErrorAction SilentlyContinue
|
|
32
|
+
if ($p -and ($p.Name -eq "antigravity-proxy" -or $p.Name -eq "bun")) {
|
|
33
|
+
Stop-Process -Id $proxyPid -Force -ErrorAction SilentlyContinue
|
|
34
|
+
$killed = $true
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (-not $killed) {
|
|
38
|
+
try {
|
|
39
|
+
$line = netstat -ano | Select-String ":$ProxyPort\s.*LISTENING" | Select-Object -First 1
|
|
40
|
+
if ($line) {
|
|
41
|
+
$ownerPid = [int](($line.ToString().Trim() -split "\s+")[-1])
|
|
42
|
+
$p = Get-Process -Id $ownerPid -ErrorAction SilentlyContinue
|
|
43
|
+
if ($p -and ($p.Name -eq "antigravity-proxy" -or $p.Name -eq "bun")) {
|
|
44
|
+
Stop-Process -Id $ownerPid -Force -ErrorAction SilentlyContinue
|
|
45
|
+
$killed = $true
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
} catch {}
|
|
49
|
+
}
|
|
50
|
+
if ($killed -and (Test-Path -LiteralPath $PidFile)) {
|
|
51
|
+
Remove-Item -LiteralPath $PidFile -Force -ErrorAction SilentlyContinue
|
|
52
|
+
}
|
|
53
|
+
return $killed
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
for ($poll = 1; $poll -le 20; $poll++) {
|
|
57
|
+
Start-Sleep -Seconds 3
|
|
58
|
+
|
|
59
|
+
# Any live openclaude process? (node/bun with "openclaude" on its command
|
|
60
|
+
# line — covers interactive, --resume, and --bg sessions. The proxy itself
|
|
61
|
+
# is "antigravity-proxy.exe" or "bun ... server.ts" and never matches.)
|
|
62
|
+
$alive = $false
|
|
63
|
+
try {
|
|
64
|
+
$procs = Get-CimInstance Win32_Process -Filter "Name = 'node.exe' OR Name = 'bun.exe'" -ErrorAction Stop
|
|
65
|
+
foreach ($proc in $procs) {
|
|
66
|
+
if ($proc.CommandLine -and $proc.CommandLine -match "openclaude") {
|
|
67
|
+
$alive = $true
|
|
68
|
+
break
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
} catch {
|
|
72
|
+
# WMI query failed — fail-safe: do not kill
|
|
73
|
+
$alive = $true
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (-not $alive) {
|
|
77
|
+
$null = Stop-Proxy
|
|
78
|
+
break
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
exit 0
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"SessionStart": [
|
|
4
|
+
{
|
|
5
|
+
"matcher": "startup",
|
|
6
|
+
"hooks": [
|
|
7
|
+
{ "type": "command", "command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/start.bat\"", "timeout": 45 }
|
|
8
|
+
]
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"matcher": "resume",
|
|
12
|
+
"hooks": [
|
|
13
|
+
{ "type": "command", "command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/start.bat\"", "timeout": 45 }
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"matcher": "clear",
|
|
18
|
+
"hooks": [
|
|
19
|
+
{ "type": "command", "command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/start.bat\"", "timeout": 45 }
|
|
20
|
+
]
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"matcher": "compact",
|
|
24
|
+
"hooks": [
|
|
25
|
+
{ "type": "command", "command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/start.bat\"", "timeout": 45 }
|
|
26
|
+
]
|
|
27
|
+
}
|
|
28
|
+
],
|
|
29
|
+
"SessionEnd": [
|
|
30
|
+
{
|
|
31
|
+
"hooks": [
|
|
32
|
+
{ "type": "command", "command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/session-end.bat\"", "timeout": 30 }
|
|
33
|
+
]
|
|
34
|
+
}
|
|
35
|
+
]
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* inject-provider.js
|
|
3
|
+
* Ensures OpenClaude's provider configuration points at the local
|
|
4
|
+
* Antigravity proxy (http://localhost:51122/v1).
|
|
5
|
+
*
|
|
6
|
+
* Idempotent: only writes when configuration is missing/mispointed.
|
|
7
|
+
* Creates .plugin-bak backups before any write. Exits 0 always.
|
|
8
|
+
*
|
|
9
|
+
* Run via: bun hooks/inject-provider.js (or: node hooks/inject-provider.js)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const fs = require("node:fs");
|
|
13
|
+
const os = require("node:os");
|
|
14
|
+
const path = require("node:path");
|
|
15
|
+
|
|
16
|
+
const HOME = os.homedir();
|
|
17
|
+
const OC_DIR = path.join(HOME, ".openclaude");
|
|
18
|
+
const PROFILE_PATH = path.join(OC_DIR, ".openclaude-profile.json");
|
|
19
|
+
const MAIN_PATH = path.join(HOME, ".openclaude.json");
|
|
20
|
+
|
|
21
|
+
const BASE_URL = "http://localhost:51122/v1";
|
|
22
|
+
const MODEL = "antigravity-claude-sonnet-4-6";
|
|
23
|
+
|
|
24
|
+
function backup(file) {
|
|
25
|
+
try {
|
|
26
|
+
if (fs.existsSync(file)) {
|
|
27
|
+
fs.copyFileSync(file, file + ".plugin-bak");
|
|
28
|
+
}
|
|
29
|
+
} catch {}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function readJson(file) {
|
|
33
|
+
try {
|
|
34
|
+
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
35
|
+
} catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function writeJson(file, data) {
|
|
41
|
+
const tmp = file + ".plugin-tmp";
|
|
42
|
+
fs.writeFileSync(tmp, JSON.stringify(data, null, 2) + "\n");
|
|
43
|
+
fs.renameSync(tmp, file);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
// ── 1. ~/.openclaude/.openclaude-profile.json (active profile env) ──────────
|
|
48
|
+
let needProfile = true;
|
|
49
|
+
const existing = readJson(PROFILE_PATH);
|
|
50
|
+
if (
|
|
51
|
+
existing &&
|
|
52
|
+
existing.env &&
|
|
53
|
+
typeof existing.env.OPENAI_BASE_URL === "string" &&
|
|
54
|
+
existing.env.OPENAI_BASE_URL.includes(":51122")
|
|
55
|
+
) {
|
|
56
|
+
needProfile = false;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (needProfile) {
|
|
60
|
+
backup(PROFILE_PATH);
|
|
61
|
+
fs.mkdirSync(OC_DIR, { recursive: true });
|
|
62
|
+
writeJson(PROFILE_PATH, {
|
|
63
|
+
profile: "openai",
|
|
64
|
+
env: {
|
|
65
|
+
OPENAI_BASE_URL: BASE_URL,
|
|
66
|
+
OPENAI_MODEL: MODEL,
|
|
67
|
+
OPENAI_AUTH_HEADER: "dummy",
|
|
68
|
+
OPENAI_AUTH_SCHEME: "raw",
|
|
69
|
+
},
|
|
70
|
+
createdAt: existing && existing.createdAt ? existing.createdAt : new Date().toISOString(),
|
|
71
|
+
});
|
|
72
|
+
console.log("[antigravity-provider] injected provider profile (.openclaude-profile.json)");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ── 2. ~/.openclaude.json providerProfiles entry (only when absent) ────────
|
|
76
|
+
// Never overrides the user's active provider if an entry already exists —
|
|
77
|
+
// they may have deliberately switched. Injection is for fresh setups only.
|
|
78
|
+
if (fs.existsSync(MAIN_PATH)) {
|
|
79
|
+
const main = readJson(MAIN_PATH);
|
|
80
|
+
if (main) {
|
|
81
|
+
const profiles = Array.isArray(main.providerProfiles) ? main.providerProfiles : [];
|
|
82
|
+
const has = profiles.some(
|
|
83
|
+
(p) => p && typeof p.baseUrl === "string" && p.baseUrl.includes(":51122"),
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
if (!has) {
|
|
87
|
+
backup(MAIN_PATH);
|
|
88
|
+
const id = "provider_antigravity_local";
|
|
89
|
+
profiles.push({
|
|
90
|
+
id: id,
|
|
91
|
+
name: "Antigravity (Local Proxy)",
|
|
92
|
+
provider: "custom",
|
|
93
|
+
baseUrl: BASE_URL,
|
|
94
|
+
model: MODEL,
|
|
95
|
+
authHeader: "dummy",
|
|
96
|
+
authScheme: "raw",
|
|
97
|
+
});
|
|
98
|
+
main.providerProfiles = profiles;
|
|
99
|
+
main.activeProviderProfileId = id;
|
|
100
|
+
writeJson(MAIN_PATH, main);
|
|
101
|
+
console.log("[antigravity-provider] injected providerProfiles entry (.openclaude.json)");
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
} catch (e) {
|
|
106
|
+
// Never fail the hook because of injection problems.
|
|
107
|
+
console.error("[antigravity-provider] provider inject skipped: " + (e && e.message));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
process.exit(0);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
@echo off
|
|
2
|
+
rem session-end.bat - OpenClaude SessionEnd hook wrapper.
|
|
3
|
+
rem Spawns the detached Watchdog-Stop.ps1 which stops the Antigravity proxy
|
|
4
|
+
rem once no openclaude process remains alive.
|
|
5
|
+
rem
|
|
6
|
+
rem IMPORTANT: OpenClaude aborts SessionEnd hooks after ~1.5s
|
|
7
|
+
rem (SESSION_END_HOOK_TIMEOUT_MS_DEFAULT). This wrapper therefore spawns the
|
|
8
|
+
rem watchdog as FAST as possible: wmic first (no PowerShell cold-start,
|
|
9
|
+
rem ~0.4s), PowerShell+WMI as fallback when wmic is unavailable.
|
|
10
|
+
rem The watchdog itself is created by the WMI service, fully detached from
|
|
11
|
+
rem this process tree, so it survives this hook being cancelled.
|
|
12
|
+
|
|
13
|
+
set "WATCHDOG=%~dp0Watchdog-Stop.ps1"
|
|
14
|
+
|
|
15
|
+
where wmic >nul 2>&1
|
|
16
|
+
if %errorlevel%==0 (
|
|
17
|
+
wmic process call create "powershell -NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File %WATCHDOG%" >nul 2>&1
|
|
18
|
+
exit /b 0
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
powershell -NoProfile -ExecutionPolicy Bypass -Command "try { $null = Invoke-WmiMethod -Class Win32_Process -Name Create -ArgumentList ('powershell -NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File ' + [char]34 + '%WATCHDOG%' + [char]34) } catch {}"
|
|
22
|
+
exit /b 0
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
@echo off
|
|
2
|
+
rem start.bat - OpenClaude SessionStart hook wrapper.
|
|
3
|
+
rem Simple entry point that OpenClaude's command runner can execute on Windows.
|
|
4
|
+
rem Delegates to SessionStart.ps1 which ensures the Antigravity proxy is running.
|
|
5
|
+
|
|
6
|
+
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0SessionStart.ps1"
|
|
7
|
+
exit /b 0
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "openclaude-antigravity-provider",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Local OpenAI-compatible proxy that authenticates via Google Antigravity OAuth, enabling OpenClaude to use Claude Opus/Sonnet and Gemini models with your Google credentials.",
|
|
5
|
+
"module": "src/server.ts",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"engines": {
|
|
9
|
+
"bun": ">=1.0.0"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"start": "bun run src/server.ts",
|
|
13
|
+
"auth": "bun run src/auth-cli.ts",
|
|
14
|
+
"build": "bun build --compile src/server.ts --outfile bin/antigravity-proxy.exe",
|
|
15
|
+
"typecheck": "bun tsc --noEmit"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"open": "^10.1.0"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@types/bun": "latest",
|
|
22
|
+
"typescript": "^5.6.0"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* accounts.ts
|
|
3
|
+
* Multi-account manager with in-memory access token cache,
|
|
4
|
+
* automatic rate-limit rotation, and least-recently-used selection.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { loadAccounts, saveAccounts, type StoredAccount } from "./storage.ts";
|
|
8
|
+
import { refreshAccessToken, accessTokenExpired } from "./auth.ts";
|
|
9
|
+
|
|
10
|
+
interface CachedToken {
|
|
11
|
+
access: string;
|
|
12
|
+
expires: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// In-memory token cache — keyed by refreshToken string
|
|
16
|
+
const tokenCache = new Map<string, CachedToken>();
|
|
17
|
+
|
|
18
|
+
// ── Token retrieval with auto-refresh ─────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
export async function getValidAccessToken(
|
|
21
|
+
account: StoredAccount,
|
|
22
|
+
): Promise<string> {
|
|
23
|
+
const cached = tokenCache.get(account.refreshToken);
|
|
24
|
+
if (cached && !accessTokenExpired(cached.expires)) {
|
|
25
|
+
return cached.access;
|
|
26
|
+
}
|
|
27
|
+
const result = await refreshAccessToken(account.refreshToken);
|
|
28
|
+
tokenCache.set(account.refreshToken, result);
|
|
29
|
+
return result.access;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ── Account selection (LRU, skips rate-limited/disabled) ─────────────────────
|
|
33
|
+
|
|
34
|
+
export async function getAvailableAccount(): Promise<{
|
|
35
|
+
account: StoredAccount;
|
|
36
|
+
index: number;
|
|
37
|
+
} | null> {
|
|
38
|
+
const data = await loadAccounts();
|
|
39
|
+
if (!data || data.accounts.length === 0) return null;
|
|
40
|
+
|
|
41
|
+
const now = Date.now();
|
|
42
|
+
|
|
43
|
+
const active = data.accounts
|
|
44
|
+
.map((acc, index) => ({ acc, index }))
|
|
45
|
+
.filter(({ acc }) => {
|
|
46
|
+
if (!acc.enabled) return false;
|
|
47
|
+
if (acc.rateLimitedUntil && acc.rateLimitedUntil > now) return false;
|
|
48
|
+
return true;
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
if (active.length === 0) {
|
|
52
|
+
// All limited — find the one whose limit expires soonest.
|
|
53
|
+
const soonest = data.accounts
|
|
54
|
+
.filter((a) => a.enabled && a.rateLimitedUntil)
|
|
55
|
+
.sort((a, b) => (a.rateLimitedUntil ?? 0) - (b.rateLimitedUntil ?? 0))[0];
|
|
56
|
+
|
|
57
|
+
if (soonest?.rateLimitedUntil) {
|
|
58
|
+
const waitMs = soonest.rateLimitedUntil - now;
|
|
59
|
+
// If the wait is short enough (<= 2 min), sleep and retry automatically
|
|
60
|
+
// instead of crashing — mirrors the opencode-antigravity-auth plugin behavior.
|
|
61
|
+
if (waitMs > 0 && waitMs <= 120_000) {
|
|
62
|
+
console.log(
|
|
63
|
+
`[antigravity-provider] All accounts rate-limited. Sleeping ${Math.ceil(waitMs / 1000)}s...`,
|
|
64
|
+
);
|
|
65
|
+
await new Promise<void>((r) => setTimeout(r, waitMs + 200)); // +200ms buffer
|
|
66
|
+
return getAvailableAccount(); // retry after sleep
|
|
67
|
+
}
|
|
68
|
+
const waitSec = Math.ceil(waitMs / 1000);
|
|
69
|
+
throw new Error(
|
|
70
|
+
`All accounts are rate-limited. Next available in ${waitSec}s.`,
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Pick least-recently-used
|
|
77
|
+
const best = active.sort(
|
|
78
|
+
(a, b) => (a.acc.lastUsed ?? 0) - (b.acc.lastUsed ?? 0),
|
|
79
|
+
)[0]!;
|
|
80
|
+
|
|
81
|
+
return { account: best.acc, index: best.index };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ── Mark account rate-limited ─────────────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
export async function markAccountRateLimited(
|
|
87
|
+
index: number,
|
|
88
|
+
retryAfterMs: number,
|
|
89
|
+
): Promise<void> {
|
|
90
|
+
const data = await loadAccounts();
|
|
91
|
+
if (!data || !data.accounts[index]) return;
|
|
92
|
+
data.accounts[index]!.rateLimitedUntil = Date.now() + retryAfterMs;
|
|
93
|
+
tokenCache.delete(data.accounts[index]!.refreshToken);
|
|
94
|
+
await saveAccounts(data);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ── Mark account as successfully used ────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
export async function markAccountUsed(index: number): Promise<void> {
|
|
100
|
+
const data = await loadAccounts();
|
|
101
|
+
if (!data || !data.accounts[index]) return;
|
|
102
|
+
data.accounts[index]!.lastUsed = Date.now();
|
|
103
|
+
data.accounts[index]!.rateLimitedUntil = undefined;
|
|
104
|
+
await saveAccounts(data);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export async function getAccountCount(): Promise<number> {
|
|
108
|
+
const data = await loadAccounts();
|
|
109
|
+
return data?.accounts.length ?? 0;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export async function getAllAccounts(): Promise<StoredAccount[]> {
|
|
113
|
+
const data = await loadAccounts();
|
|
114
|
+
return data?.accounts ?? [];
|
|
115
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* auth-cli.ts
|
|
3
|
+
* Interactive first-time Google OAuth login for OpenClaude Antigravity Provider.
|
|
4
|
+
* Run: bun run src/auth-cli.ts
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { loadAccounts, saveAccounts, type StoredAccount } from "./storage.ts";
|
|
8
|
+
import {
|
|
9
|
+
buildAuthorizationUrl,
|
|
10
|
+
waitForOAuthCallback,
|
|
11
|
+
exchangeCodeForTokens,
|
|
12
|
+
} from "./auth.ts";
|
|
13
|
+
import { ACCOUNTS_FILE } from "./constants.ts";
|
|
14
|
+
|
|
15
|
+
async function openBrowser(url: string): Promise<void> {
|
|
16
|
+
try {
|
|
17
|
+
if (process.platform === "win32") {
|
|
18
|
+
// cmd.exe splits URLs at '&' — use PowerShell Start-Process instead
|
|
19
|
+
// which correctly passes the full URL as a single string argument.
|
|
20
|
+
Bun.spawn(
|
|
21
|
+
["powershell", "-NoProfile", "-NonInteractive", "-Command",
|
|
22
|
+
`Start-Process "${url.replace(/"/g, '`"')}"`,
|
|
23
|
+
],
|
|
24
|
+
{ stdout: "ignore", stderr: "ignore" },
|
|
25
|
+
);
|
|
26
|
+
} else if (process.platform === "darwin") {
|
|
27
|
+
Bun.spawn(["open", url], { stdout: "ignore", stderr: "ignore" });
|
|
28
|
+
} else {
|
|
29
|
+
Bun.spawn(["xdg-open", url], { stdout: "ignore", stderr: "ignore" });
|
|
30
|
+
}
|
|
31
|
+
} catch {
|
|
32
|
+
// Browser open failed — user will copy URL manually
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function main(): Promise<void> {
|
|
37
|
+
console.log("\n=== OpenClaude Antigravity Auth ===\n");
|
|
38
|
+
|
|
39
|
+
const existing = await loadAccounts();
|
|
40
|
+
if (existing && existing.accounts.length > 0) {
|
|
41
|
+
console.log(`Found ${existing.accounts.length} existing account(s):`);
|
|
42
|
+
for (const acc of existing.accounts) {
|
|
43
|
+
const status = acc.enabled ? "enabled" : "disabled";
|
|
44
|
+
console.log(` - ${acc.email ?? "unknown"} [${status}]`);
|
|
45
|
+
}
|
|
46
|
+
console.log("\nAdding / re-authenticating...\n");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const { url, verifier, state } = await buildAuthorizationUrl();
|
|
50
|
+
|
|
51
|
+
console.log("Opening your browser for Google sign-in...");
|
|
52
|
+
console.log("If the browser does not open, paste this URL manually:\n");
|
|
53
|
+
console.log(url);
|
|
54
|
+
console.log();
|
|
55
|
+
|
|
56
|
+
await openBrowser(url);
|
|
57
|
+
|
|
58
|
+
console.log(
|
|
59
|
+
`Waiting for OAuth callback on http://localhost:51121/oauth-callback ...\n`,
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
let code: string;
|
|
63
|
+
try {
|
|
64
|
+
const result = await waitForOAuthCallback(state);
|
|
65
|
+
code = result.code;
|
|
66
|
+
} catch (err: unknown) {
|
|
67
|
+
console.error(
|
|
68
|
+
"OAuth callback failed:",
|
|
69
|
+
err instanceof Error ? err.message : String(err),
|
|
70
|
+
);
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
console.log("Exchanging authorization code for tokens...");
|
|
75
|
+
|
|
76
|
+
let tokens: Awaited<ReturnType<typeof exchangeCodeForTokens>>;
|
|
77
|
+
try {
|
|
78
|
+
tokens = await exchangeCodeForTokens(code, verifier);
|
|
79
|
+
} catch (err: unknown) {
|
|
80
|
+
console.error(
|
|
81
|
+
"Token exchange failed:",
|
|
82
|
+
err instanceof Error ? err.message : String(err),
|
|
83
|
+
);
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const data = existing ?? { version: 1, accounts: [] as StoredAccount[], activeIndex: 0 };
|
|
88
|
+
|
|
89
|
+
// Update existing account if same email, otherwise add new
|
|
90
|
+
const existingIdx = tokens.email
|
|
91
|
+
? data.accounts.findIndex((a) => a.email === tokens.email)
|
|
92
|
+
: -1;
|
|
93
|
+
|
|
94
|
+
const now = Date.now();
|
|
95
|
+
const newAccount: StoredAccount = {
|
|
96
|
+
email: tokens.email,
|
|
97
|
+
refreshToken: tokens.refresh_token,
|
|
98
|
+
addedAt: existingIdx >= 0 ? (data.accounts[existingIdx]!.addedAt) : now,
|
|
99
|
+
lastUsed: now,
|
|
100
|
+
enabled: true,
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
if (existingIdx >= 0) {
|
|
104
|
+
data.accounts[existingIdx] = newAccount;
|
|
105
|
+
console.log(`\nUpdated existing account: ${tokens.email ?? "unknown"}`);
|
|
106
|
+
} else {
|
|
107
|
+
data.accounts.push(newAccount);
|
|
108
|
+
console.log(`\nAdded new account: ${tokens.email ?? "unknown"}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
await saveAccounts(data);
|
|
112
|
+
|
|
113
|
+
console.log(`\nAccounts saved to: ${ACCOUNTS_FILE}`);
|
|
114
|
+
console.log(`Total accounts: ${data.accounts.length}`);
|
|
115
|
+
console.log("\nStart the proxy server with:");
|
|
116
|
+
console.log(" bun run src/server.ts\n");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
main().catch((err: unknown) => {
|
|
120
|
+
console.error(
|
|
121
|
+
"Fatal error:",
|
|
122
|
+
err instanceof Error ? err.message : String(err),
|
|
123
|
+
);
|
|
124
|
+
process.exit(1);
|
|
125
|
+
});
|