@kontextmind/kxm 0.7.15 → 0.7.16
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/.claude-plugin/marketplace.json +1 -1
- package/docs/configuration.md +1 -2
- package/docs/operator-pi-packages.md +5 -2
- package/package.json +1 -1
- package/plugins/kxm/.claude-plugin/plugin.json +1 -1
- package/plugins/kxm/dist/cli.js +4 -0
- package/plugins/kxm/dist/core.js +4 -0
- package/plugins/kxm/dist/extension.js +3066 -2
- package/plugins/kxm/dist/mcp-server.js +1 -1
- package/plugins/kxm/dist/runtime.js +4 -0
- package/plugins/kxm/dist/server.js +4 -0
- package/plugins/kxm/package.json +1 -1
- package/plugins/kxm/src/extension.ts +9 -0
- package/plugins/kxm/src/mcp-server.ts +1 -1
- package/plugins/kxm/src/providers/antigravity/LICENSE +21 -0
- package/plugins/kxm/src/providers/antigravity/README.md +19 -0
- package/plugins/kxm/src/providers/antigravity/auth/index.ts +17 -0
- package/plugins/kxm/src/providers/antigravity/auth/oauth.ts +449 -0
- package/plugins/kxm/src/providers/antigravity/client/client.ts +562 -0
- package/plugins/kxm/src/providers/antigravity/client/index.ts +1 -0
- package/plugins/kxm/src/providers/antigravity/diagnostics/diagnostics.ts +97 -0
- package/plugins/kxm/src/providers/antigravity/diagnostics/index.ts +1 -0
- package/plugins/kxm/src/providers/antigravity/index.ts +24 -0
- package/plugins/kxm/src/providers/antigravity/models/discovery.ts +155 -0
- package/plugins/kxm/src/providers/antigravity/models/grouping.ts +425 -0
- package/plugins/kxm/src/providers/antigravity/models/index.ts +3 -0
- package/plugins/kxm/src/providers/antigravity/models/models.ts +501 -0
- package/plugins/kxm/src/providers/antigravity/pi-compat.ts +279 -0
- package/plugins/kxm/src/providers/antigravity/register.ts +123 -0
- package/plugins/kxm/src/providers/antigravity/stream/index.ts +1 -0
- package/plugins/kxm/src/providers/antigravity/stream/stream.ts +1461 -0
- package/plugins/kxm/src/providers/antigravity/types/enums.ts +50 -0
- package/plugins/kxm/src/providers/antigravity/types/index.ts +2 -0
- package/plugins/kxm/src/providers/antigravity/types/types.ts +293 -0
- package/plugins/kxm/src/providers/antigravity/usage/index.ts +1 -0
- package/plugins/kxm/src/providers/antigravity/usage/usage.ts +361 -0
- package/plugins/kxm/src/providers/antigravity/utils/http.ts +32 -0
- package/plugins/kxm/src/providers/antigravity/utils/index.ts +3 -0
- package/plugins/kxm/src/providers/antigravity/utils/security.ts +75 -0
- package/plugins/kxm/src/providers/antigravity/utils/util.ts +133 -0
- package/plugins/kxm/src/redact.ts +4 -0
|
@@ -17036,7 +17036,7 @@ async function deliverInboxNotification(messageId, delivered, notify) {
|
|
|
17036
17036
|
}
|
|
17037
17037
|
|
|
17038
17038
|
// plugins/kxm/src/mcp-server.ts
|
|
17039
|
-
var VERSION = "0.7.
|
|
17039
|
+
var VERSION = "0.7.16";
|
|
17040
17040
|
var inbox = /* @__PURE__ */ new Map();
|
|
17041
17041
|
var notifiedInbox = /* @__PURE__ */ new Set();
|
|
17042
17042
|
var meshClient;
|
|
@@ -27560,6 +27560,10 @@ var SECRET_PATTERNS = [
|
|
|
27560
27560
|
/\bgithub_pat_[A-Za-z0-9_]{20,}\b/g,
|
|
27561
27561
|
/\bxox[baprs]-[A-Za-z0-9-]{10,}\b/gi,
|
|
27562
27562
|
/\bBearer\s+[A-Za-z0-9._~+/=-]+\b/gi,
|
|
27563
|
+
/\bya29\.[A-Za-z0-9._~+/-]+=*/g,
|
|
27564
|
+
/\b1\/\/[A-Za-z0-9_-]+/g,
|
|
27565
|
+
/\b1\/[A-Za-z0-9_-]{20,}/g,
|
|
27566
|
+
/("?(?:access_token|refresh_token|id_token)"?\s*[:=]\s*")[^"]*(")/gi,
|
|
27563
27567
|
/\bKXM_[A-Z0-9_]*(TOKEN|SECRET|KEY)[A-Z0-9_]*=\S+/gi,
|
|
27564
27568
|
/\b(GITHUB_TOKEN|GH_TOKEN|KXM_AUTH_TOKEN|KXM_WORKFLOW_SIGNAL_SECRET)=\S+/gi,
|
|
27565
27569
|
/\b[A-Fa-f0-9]{64}\b/g
|
|
@@ -9215,6 +9215,10 @@ var SECRET_PATTERNS = [
|
|
|
9215
9215
|
/\bgithub_pat_[A-Za-z0-9_]{20,}\b/g,
|
|
9216
9216
|
/\bxox[baprs]-[A-Za-z0-9-]{10,}\b/gi,
|
|
9217
9217
|
/\bBearer\s+[A-Za-z0-9._~+/=-]+\b/gi,
|
|
9218
|
+
/\bya29\.[A-Za-z0-9._~+/-]+=*/g,
|
|
9219
|
+
/\b1\/\/[A-Za-z0-9_-]+/g,
|
|
9220
|
+
/\b1\/[A-Za-z0-9_-]{20,}/g,
|
|
9221
|
+
/("?(?:access_token|refresh_token|id_token)"?\s*[:=]\s*")[^"]*(")/gi,
|
|
9218
9222
|
/\bKXM_[A-Z0-9_]*(TOKEN|SECRET|KEY)[A-Z0-9_]*=\S+/gi,
|
|
9219
9223
|
/\b(GITHUB_TOKEN|GH_TOKEN|KXM_AUTH_TOKEN|KXM_WORKFLOW_SIGNAL_SECRET)=\S+/gi,
|
|
9220
9224
|
/\b[A-Fa-f0-9]{64}\b/g
|
package/plugins/kxm/package.json
CHANGED
|
@@ -5,6 +5,11 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
|
5
5
|
import { AGENT_COMMANDS, enforceToolPolicy } from "./commands.ts";
|
|
6
6
|
import { HubClient, HubHttpError } from "./client.ts";
|
|
7
7
|
import { nousFactoryWork, type NousRegistrationReport } from "./nous-pi.ts";
|
|
8
|
+
import {
|
|
9
|
+
antigravityRegistrationNotice,
|
|
10
|
+
registerAntigravityProvider,
|
|
11
|
+
type AntigravityRegistration,
|
|
12
|
+
} from "./providers/antigravity/register.ts";
|
|
8
13
|
import { areaForTool, classifyFailure, diagnosticEvidence, diagnosticSummary, type Diagnostic } from "./diagnostics.ts";
|
|
9
14
|
import {
|
|
10
15
|
MAX_CONTENT_CHARS,
|
|
@@ -160,6 +165,7 @@ export function assistantFailure(messages: unknown[]): Diagnostic | undefined {
|
|
|
160
165
|
export default function piMeshExtension(pi: ExtensionAPI): void | Promise<void> {
|
|
161
166
|
let client: HubClient | undefined;
|
|
162
167
|
let nousReport: NousRegistrationReport | undefined;
|
|
168
|
+
let antigravityReport: AntigravityRegistration | undefined;
|
|
163
169
|
let pending: MessageRecord[] = [];
|
|
164
170
|
let activatingInbound: MessageRecord | undefined;
|
|
165
171
|
let awaitingActivation: MessageRecord | undefined;
|
|
@@ -640,6 +646,8 @@ export default function piMeshExtension(pi: ExtensionAPI): void | Promise<void>
|
|
|
640
646
|
|
|
641
647
|
pi.on("session_start", async (event: { reason?: string }, ctx) => {
|
|
642
648
|
shuttingDown = false;
|
|
649
|
+
const antigravityNotice = antigravityRegistrationNotice(antigravityReport, pi);
|
|
650
|
+
if (antigravityNotice) ctx.ui.notify(antigravityNotice.message, antigravityNotice.type);
|
|
643
651
|
if (nousReport?.guidance.length) {
|
|
644
652
|
for (const item of nousReport.guidance) {
|
|
645
653
|
ctx.ui.notify(item.message, item.level);
|
|
@@ -924,6 +932,7 @@ export default function piMeshExtension(pi: ExtensionAPI): void | Promise<void>
|
|
|
924
932
|
},
|
|
925
933
|
});
|
|
926
934
|
|
|
935
|
+
antigravityReport = registerAntigravityProvider(pi);
|
|
927
936
|
return nousFactoryWork(pi, (report) => {
|
|
928
937
|
nousReport = report;
|
|
929
938
|
});
|
|
@@ -7,7 +7,7 @@ import { AGENT_COMMANDS_MAP, enforceToolPolicy, getMcpTools, reconcileInbox } fr
|
|
|
7
7
|
import { deliverInboxNotification } from "./inbox.ts";
|
|
8
8
|
import type { HubEvent, MessageRecord } from "./protocol.ts";
|
|
9
9
|
|
|
10
|
-
const VERSION = "0.7.
|
|
10
|
+
const VERSION = "0.7.16";
|
|
11
11
|
const inbox = new Map<string, MessageRecord>();
|
|
12
12
|
const notifiedInbox = new Set<string>();
|
|
13
13
|
let meshClient: HubClient | undefined;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Rahul Arya
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Antigravity provider (vendored)
|
|
2
|
+
|
|
3
|
+
Provider-only subset of `pi-antigravity`, built into the kxm plugin.
|
|
4
|
+
|
|
5
|
+
- Upstream: [Rahularya01/pi-antigravity](https://github.com/Rahularya01/pi-antigravity)
|
|
6
|
+
- KontextMind fork: [kontextmind/pi-antigravity](https://github.com/kontextmind/pi-antigravity)
|
|
7
|
+
- Fork commit: `86f76ab08f16784be5bfc281dda9c645651a433b`
|
|
8
|
+
- License: MIT, Copyright (c) 2026 Rahul Arya — see `LICENSE`
|
|
9
|
+
|
|
10
|
+
The Pi provider id remains `antigravity` so existing credentials in Pi's auth
|
|
11
|
+
store (`/login antigravity`) carry over. This slice vendors Google OAuth
|
|
12
|
+
(PKCE, `localhost:51121` callback + paste-URL fallback), Cloud Code Assist
|
|
13
|
+
streaming, dynamic model catalog with last-known-good cache and static
|
|
14
|
+
fallback, thinking-variant mapping, and quota surfaces used by the model list.
|
|
15
|
+
|
|
16
|
+
Not vendored here: image generation, `/antigravity.image`, and extra slash
|
|
17
|
+
commands (`/antigravity.models`, `/antigravity.doctor`). `usage.ts` is a dead
|
|
18
|
+
slash-command surface kept because quota parsing shares client types; it is not
|
|
19
|
+
registered. No `@earendil-works/pi-ai` import.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export {
|
|
2
|
+
loginAntigravity,
|
|
3
|
+
refreshAntigravityToken,
|
|
4
|
+
getApiKey,
|
|
5
|
+
CLIENT_ID,
|
|
6
|
+
CLIENT_SECRET,
|
|
7
|
+
REDIRECT_URI,
|
|
8
|
+
AUTH_URL,
|
|
9
|
+
TOKEN_URL,
|
|
10
|
+
SCOPES,
|
|
11
|
+
CALLBACK_HOST,
|
|
12
|
+
OAUTH_CALLBACK_TIMEOUT_MS,
|
|
13
|
+
generatePKCE,
|
|
14
|
+
buildAuthorizationUrl,
|
|
15
|
+
parsePastedCallback,
|
|
16
|
+
} from "./oauth.ts";
|
|
17
|
+
export type { OAuthCredentials, OAuthLoginCallbacks } from "./oauth.ts";
|
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
// @ts-nocheck — vendored snapshot; host tsconfig is stricter than upstream.
|
|
2
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
3
|
+
import { createServer, type Server } from "node:http";
|
|
4
|
+
import type { OAuthCredentials, OAuthLoginCallbacks } from "../pi-compat.ts";
|
|
5
|
+
import { defaultProjectId, loadCodeAssist } from "../client/client.ts";
|
|
6
|
+
import { escapeHtml, antigravityEnv } from "../utils/util.ts";
|
|
7
|
+
import { resolveCallbackHost, redactSecrets } from "../utils/security.ts";
|
|
8
|
+
import type { AntigravityOAuthCredentials, CallbackServer } from "../types/types.ts";
|
|
9
|
+
|
|
10
|
+
export const REDIRECT_URI = "http://localhost:51121/oauth-callback";
|
|
11
|
+
export const AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
|
|
12
|
+
export const TOKEN_URL = "https://oauth2.googleapis.com/token";
|
|
13
|
+
export const OAUTH_CALLBACK_TIMEOUT_MS = 5 * 60 * 1000;
|
|
14
|
+
export const SCOPES = [
|
|
15
|
+
"https://www.googleapis.com/auth/aicode",
|
|
16
|
+
"https://www.googleapis.com/auth/cloud-platform",
|
|
17
|
+
"https://www.googleapis.com/auth/userinfo.email",
|
|
18
|
+
"https://www.googleapis.com/auth/userinfo.profile",
|
|
19
|
+
"https://www.googleapis.com/auth/cclog",
|
|
20
|
+
"https://www.googleapis.com/auth/experimentsandconfigs",
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Default OAuth client is Google's public Antigravity desktop client (not a private app secret).
|
|
25
|
+
* Prefer ANTIGRAVITY_CLIENT_ID / ANTIGRAVITY_CLIENT_SECRET when you manage your own OAuth app.
|
|
26
|
+
*/
|
|
27
|
+
export const CLIENT_ID =
|
|
28
|
+
antigravityEnv("CLIENT_ID") ||
|
|
29
|
+
atob(
|
|
30
|
+
"MTA3MTAwNjA2MDU5MS10bWhzc2luMmgyMWxjcmUyMzV2dG9sb2poNGc0MDNlc" +
|
|
31
|
+
"C5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbQ==",
|
|
32
|
+
);
|
|
33
|
+
export const CLIENT_SECRET =
|
|
34
|
+
antigravityEnv("CLIENT_SECRET") || atob("R09DU1BYLUs1OEZXUjQ" + "4NkxkTEoxbUxCOHNYQzR6NnFEQWY=");
|
|
35
|
+
|
|
36
|
+
export const CALLBACK_HOST = resolveCallbackHost();
|
|
37
|
+
|
|
38
|
+
function oauthCallbackHeaders(contentType = "text/html; charset=utf-8"): Record<string, string> {
|
|
39
|
+
return {
|
|
40
|
+
"Content-Type": contentType,
|
|
41
|
+
"Cache-Control": "no-store",
|
|
42
|
+
"X-Content-Type-Options": "nosniff",
|
|
43
|
+
"Content-Security-Policy":
|
|
44
|
+
"default-src 'none'; style-src 'unsafe-inline'; base-uri 'none'; form-action 'none'",
|
|
45
|
+
"Referrer-Policy": "no-referrer",
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function sanitizeOAuthProviderError(text: string): string {
|
|
50
|
+
const redacted = redactSecrets(text).trim();
|
|
51
|
+
try {
|
|
52
|
+
const parsed = JSON.parse(redacted) as {
|
|
53
|
+
error?: string;
|
|
54
|
+
error_description?: string;
|
|
55
|
+
};
|
|
56
|
+
const parts = [parsed.error, parsed.error_description].filter(
|
|
57
|
+
(part): part is string => typeof part === "string" && part.length > 0,
|
|
58
|
+
);
|
|
59
|
+
if (parts.length) return parts.join(": ").slice(0, 300);
|
|
60
|
+
} catch {
|
|
61
|
+
// not JSON
|
|
62
|
+
}
|
|
63
|
+
return redacted.slice(0, 300) || "unknown OAuth provider error";
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function base64Url(buffer: Buffer): string {
|
|
67
|
+
return buffer.toString("base64url");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function generatePKCE(): { verifier: string; challenge: string } {
|
|
71
|
+
const verifier = base64Url(randomBytes(32));
|
|
72
|
+
const challenge = base64Url(createHash("sha256").update(verifier).digest());
|
|
73
|
+
return { verifier, challenge };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function buildAuthorizationUrl(challenge: string, state: string): string {
|
|
77
|
+
const authParams = new URLSearchParams({
|
|
78
|
+
client_id: CLIENT_ID,
|
|
79
|
+
response_type: "code",
|
|
80
|
+
redirect_uri: REDIRECT_URI,
|
|
81
|
+
scope: SCOPES.join(" "),
|
|
82
|
+
code_challenge: challenge,
|
|
83
|
+
code_challenge_method: "S256",
|
|
84
|
+
state,
|
|
85
|
+
access_type: "offline",
|
|
86
|
+
prompt: "consent",
|
|
87
|
+
});
|
|
88
|
+
return `${AUTH_URL}?${authParams.toString()}`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function getUserEmail(token: string): Promise<string | undefined> {
|
|
92
|
+
try {
|
|
93
|
+
const res = await fetch("https://www.googleapis.com/oauth2/v1/userinfo?alt=json", {
|
|
94
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
95
|
+
});
|
|
96
|
+
if (!res.ok) return undefined;
|
|
97
|
+
const data = (await res.json()) as { email?: string };
|
|
98
|
+
return data.email;
|
|
99
|
+
} catch {
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function closeServerGracefully(server: Server): void {
|
|
105
|
+
if ("closeAllConnections" in server && typeof server.closeAllConnections === "function") {
|
|
106
|
+
server.closeAllConnections();
|
|
107
|
+
}
|
|
108
|
+
server.close();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function startCallbackServer(expectedState: string): Promise<CallbackServer> {
|
|
112
|
+
return new Promise((resolve, reject) => {
|
|
113
|
+
let settled = false;
|
|
114
|
+
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
115
|
+
let resolveCode!: (value: { code: string; state: string }) => void;
|
|
116
|
+
let rejectCode!: (error: Error) => void;
|
|
117
|
+
const codePromise = new Promise<{ code: string; state: string }>((res, rej) => {
|
|
118
|
+
resolveCode = res;
|
|
119
|
+
rejectCode = rej;
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const finish = (fn: () => void) => {
|
|
123
|
+
if (settled) return;
|
|
124
|
+
settled = true;
|
|
125
|
+
if (timeout) clearTimeout(timeout);
|
|
126
|
+
fn();
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const server = createServer((req, res) => {
|
|
130
|
+
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
131
|
+
res.writeHead(405, oauthCallbackHeaders("text/plain; charset=utf-8"));
|
|
132
|
+
res.end("Method Not Allowed");
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const url = new URL(req.url || "", REDIRECT_URI);
|
|
137
|
+
if (url.pathname !== "/oauth-callback") {
|
|
138
|
+
res.writeHead(404, oauthCallbackHeaders());
|
|
139
|
+
res.end("Antigravity OAuth callback route not found.");
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const error = url.searchParams.get("error");
|
|
144
|
+
const code = url.searchParams.get("code");
|
|
145
|
+
const state = url.searchParams.get("state");
|
|
146
|
+
if (error) {
|
|
147
|
+
const safe = escapeHtml(error.slice(0, 200));
|
|
148
|
+
res.writeHead(400, oauthCallbackHeaders());
|
|
149
|
+
res.end(`Antigravity authentication failed: ${safe}`);
|
|
150
|
+
finish(() => rejectCode(new Error(`OAuth error: ${error.slice(0, 200)}`)));
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (!code || !state) {
|
|
154
|
+
res.writeHead(400, oauthCallbackHeaders());
|
|
155
|
+
res.end("Antigravity authentication failed: missing code or state.");
|
|
156
|
+
finish(() => rejectCode(new Error("Missing code or state in OAuth callback")));
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
if (state !== expectedState) {
|
|
160
|
+
res.writeHead(400, oauthCallbackHeaders());
|
|
161
|
+
res.end("Antigravity authentication failed: invalid state.");
|
|
162
|
+
finish(() => rejectCode(new Error("OAuth state mismatch")));
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
res.writeHead(200, oauthCallbackHeaders());
|
|
167
|
+
res.end("Antigravity authentication complete. You can close this window and return to Pi.");
|
|
168
|
+
finish(() => resolveCode({ code, state }));
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
server.on("error", (err: NodeJS.ErrnoException) => {
|
|
172
|
+
if (err.code === "EADDRINUSE") {
|
|
173
|
+
reject(
|
|
174
|
+
new Error(
|
|
175
|
+
"Port 51121 is already in use by another process. Please close the process using port 51121 and retry /login antigravity.",
|
|
176
|
+
),
|
|
177
|
+
);
|
|
178
|
+
} else {
|
|
179
|
+
reject(err);
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
let closed = false;
|
|
184
|
+
const close = () => {
|
|
185
|
+
if (closed) return;
|
|
186
|
+
closed = true;
|
|
187
|
+
closeServerGracefully(server);
|
|
188
|
+
};
|
|
189
|
+
const cleanup = () => {
|
|
190
|
+
finish(() => rejectCode(new Error("OAuth callback cancelled")));
|
|
191
|
+
close();
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
server.listen(51121, CALLBACK_HOST, () => {
|
|
195
|
+
timeout = setTimeout(() => {
|
|
196
|
+
finish(() => rejectCode(new Error("OAuth callback timed out waiting for browser login")));
|
|
197
|
+
close();
|
|
198
|
+
}, OAUTH_CALLBACK_TIMEOUT_MS);
|
|
199
|
+
resolve({ server, waitForCode: () => codePromise, cleanup });
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function credentialProjectId(credentials: OAuthCredentials): string | undefined {
|
|
205
|
+
const projectId = (credentials as AntigravityOAuthCredentials).projectId;
|
|
206
|
+
return typeof projectId === "string" ? projectId : undefined;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function credentialEmail(credentials: OAuthCredentials): string | undefined {
|
|
210
|
+
const email = (credentials as AntigravityOAuthCredentials).email;
|
|
211
|
+
return typeof email === "string" ? email : undefined;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Parse a callback URL (or bare query string) pasted by the user from a
|
|
216
|
+
* remote/headless browser that could not reach the local callback server.
|
|
217
|
+
* Returns the validated {code, state} or throws a descriptive error. The checks
|
|
218
|
+
* mirror the local callback server so a pasted URL is accepted under the exact
|
|
219
|
+
* same rules as a same-machine browser redirect.
|
|
220
|
+
*/
|
|
221
|
+
export function parsePastedCallback(raw: string, expectedState: string): { code: string; state: string } {
|
|
222
|
+
const text = (raw ?? "").trim();
|
|
223
|
+
if (!text) {
|
|
224
|
+
throw new Error(
|
|
225
|
+
"No callback pasted. Paste the full URL from your browser's address bar (http://localhost:51121/oauth-callback?…).",
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
let url: URL;
|
|
229
|
+
try {
|
|
230
|
+
url = new URL(text);
|
|
231
|
+
} catch {
|
|
232
|
+
// Bare query string: "state=…&code=…" or "?state=…&code=…"
|
|
233
|
+
const qs = text.startsWith("?") ? text.slice(1) : text;
|
|
234
|
+
url = new URL(`http://localhost:51121/oauth-callback?${qs}`);
|
|
235
|
+
}
|
|
236
|
+
const error = url.searchParams.get("error");
|
|
237
|
+
if (error) throw new Error(`OAuth error from browser: ${error.slice(0, 200)}`);
|
|
238
|
+
const code = url.searchParams.get("code");
|
|
239
|
+
const state = url.searchParams.get("state");
|
|
240
|
+
if (!code || !state) {
|
|
241
|
+
throw new Error(
|
|
242
|
+
"Pasted text is missing 'code' or 'state'. Paste the FULL callback URL from your browser's address bar (http://localhost:51121/oauth-callback?…).",
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
if (state !== expectedState) {
|
|
246
|
+
throw new Error(
|
|
247
|
+
"State mismatch: that callback is from a different sign-in. Re-run /login antigravity, sign in again, and paste the new URL.",
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
return { code, state };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Resolve the authorization code from either the local callback server (browser
|
|
255
|
+
* on the same machine) or a pasted callback URL (browser on a remote/headless
|
|
256
|
+
* machine that cannot reach localhost:51121). Whichever provides a valid code
|
|
257
|
+
* first wins; the losing path is cancelled. Honors callbacks.signal for cancel.
|
|
258
|
+
*/
|
|
259
|
+
async function acquireAuthCode(
|
|
260
|
+
expectedState: string,
|
|
261
|
+
opts: {
|
|
262
|
+
waitForCode: () => Promise<{ code: string; state: string }>;
|
|
263
|
+
callbacks: OAuthLoginCallbacks;
|
|
264
|
+
},
|
|
265
|
+
): Promise<{ code: string; state: string }> {
|
|
266
|
+
const { waitForCode, callbacks } = opts;
|
|
267
|
+
// Signals the losing candidate(s) to stop once the race settles.
|
|
268
|
+
const settle = new AbortController();
|
|
269
|
+
const candidates: Promise<{ code: string; state: string }>[] = [waitForCode()];
|
|
270
|
+
|
|
271
|
+
// Manual paste path: offered whenever the callback surface can prompt.
|
|
272
|
+
if (typeof callbacks.onPrompt === "function") {
|
|
273
|
+
const promptLoop = async (): Promise<{ code: string; state: string }> => {
|
|
274
|
+
let promptMessage =
|
|
275
|
+
"Remote/headless machine (your browser can't reach localhost:51121)? " +
|
|
276
|
+
"After signing in, paste the full callback URL shown in your browser's address bar.";
|
|
277
|
+
while (true) {
|
|
278
|
+
if (callbacks.signal?.aborted || settle.signal.aborted) {
|
|
279
|
+
throw new Error("Login cancelled");
|
|
280
|
+
}
|
|
281
|
+
let text: string;
|
|
282
|
+
try {
|
|
283
|
+
text = await callbacks.onPrompt({
|
|
284
|
+
message: promptMessage,
|
|
285
|
+
placeholder: "http://localhost:51121/oauth-callback?state=…&code=…",
|
|
286
|
+
});
|
|
287
|
+
} catch (err: unknown) {
|
|
288
|
+
// Prompt/transport failure — propagate, do not retry.
|
|
289
|
+
throw err instanceof Error ? err : new Error(String(err));
|
|
290
|
+
}
|
|
291
|
+
if (callbacks.signal?.aborted || settle.signal.aborted) {
|
|
292
|
+
throw new Error("Login cancelled");
|
|
293
|
+
}
|
|
294
|
+
try {
|
|
295
|
+
return parsePastedCallback(text, expectedState);
|
|
296
|
+
} catch (err: unknown) {
|
|
297
|
+
if (callbacks.signal?.aborted || settle.signal.aborted) {
|
|
298
|
+
throw new Error("Login cancelled", { cause: err });
|
|
299
|
+
}
|
|
300
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
301
|
+
promptMessage = `Invalid callback (${errorMessage}). Please paste the full URL:`;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
candidates.push(promptLoop());
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Flow cancellation (escape) aborts the whole login when a signal is provided.
|
|
309
|
+
let removeAbortListener: (() => void) | undefined;
|
|
310
|
+
if (callbacks.signal) {
|
|
311
|
+
candidates.push(
|
|
312
|
+
new Promise<never>((_resolve, reject) => {
|
|
313
|
+
const signal = callbacks.signal as AbortSignal;
|
|
314
|
+
if (signal.aborted) return reject(new Error("Login cancelled"));
|
|
315
|
+
const onAbort = () => reject(new Error("Login cancelled"));
|
|
316
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
317
|
+
removeAbortListener = () => signal.removeEventListener("abort", onAbort);
|
|
318
|
+
}),
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Suppress unhandled rejections from losing candidates after the race settles.
|
|
323
|
+
for (const candidate of candidates) {
|
|
324
|
+
candidate.catch(() => {});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
try {
|
|
328
|
+
return await Promise.race(candidates);
|
|
329
|
+
} finally {
|
|
330
|
+
settle.abort();
|
|
331
|
+
removeAbortListener?.();
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export async function loginAntigravity(
|
|
336
|
+
callbacks: OAuthLoginCallbacks,
|
|
337
|
+
): Promise<AntigravityOAuthCredentials> {
|
|
338
|
+
const { verifier, challenge } = generatePKCE();
|
|
339
|
+
// State must be independent of the PKCE verifier so a leaked callback URL
|
|
340
|
+
// cannot also disclose the code_verifier needed to mint tokens.
|
|
341
|
+
const state = base64Url(randomBytes(32));
|
|
342
|
+
const { waitForCode, cleanup } = await startCallbackServer(state);
|
|
343
|
+
try {
|
|
344
|
+
callbacks.onAuth({
|
|
345
|
+
url: buildAuthorizationUrl(challenge, state),
|
|
346
|
+
instructions:
|
|
347
|
+
"Complete Google sign-in. Pi captures the local callback automatically — or, on a remote/headless machine, paste the callback URL when prompted.",
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
const { code, state: returnedState } = await acquireAuthCode(state, {
|
|
351
|
+
waitForCode,
|
|
352
|
+
callbacks,
|
|
353
|
+
});
|
|
354
|
+
if (returnedState !== state) throw new Error("OAuth state mismatch");
|
|
355
|
+
|
|
356
|
+
const tokenResponse = await fetch(TOKEN_URL, {
|
|
357
|
+
method: "POST",
|
|
358
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
359
|
+
body: new URLSearchParams({
|
|
360
|
+
client_id: CLIENT_ID,
|
|
361
|
+
client_secret: CLIENT_SECRET,
|
|
362
|
+
code,
|
|
363
|
+
grant_type: "authorization_code",
|
|
364
|
+
redirect_uri: REDIRECT_URI,
|
|
365
|
+
code_verifier: verifier,
|
|
366
|
+
}).toString(),
|
|
367
|
+
});
|
|
368
|
+
if (!tokenResponse.ok) {
|
|
369
|
+
throw new Error(
|
|
370
|
+
`Token exchange failed: ${sanitizeOAuthProviderError(await tokenResponse.text())}`,
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
const tokenData = (await tokenResponse.json()) as {
|
|
374
|
+
access_token: string;
|
|
375
|
+
refresh_token?: string;
|
|
376
|
+
expires_in: number;
|
|
377
|
+
};
|
|
378
|
+
if (!tokenData.refresh_token) {
|
|
379
|
+
throw new Error(
|
|
380
|
+
"No refresh token received. Re-run /login antigravity and allow offline access.",
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const [email, discoveredProject] = await Promise.all([
|
|
385
|
+
getUserEmail(tokenData.access_token),
|
|
386
|
+
loadCodeAssist(tokenData.access_token),
|
|
387
|
+
]);
|
|
388
|
+
return {
|
|
389
|
+
refresh: tokenData.refresh_token,
|
|
390
|
+
access: tokenData.access_token,
|
|
391
|
+
expires: Date.now() + tokenData.expires_in * 1000 - 5 * 60 * 1000,
|
|
392
|
+
projectId: discoveredProject || defaultProjectId(email || "antigravity-default"),
|
|
393
|
+
email,
|
|
394
|
+
};
|
|
395
|
+
} finally {
|
|
396
|
+
cleanup();
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
export async function refreshAntigravityToken(
|
|
401
|
+
credentials: OAuthCredentials,
|
|
402
|
+
signal?: AbortSignal,
|
|
403
|
+
): Promise<AntigravityOAuthCredentials> {
|
|
404
|
+
const response = await fetch(TOKEN_URL, {
|
|
405
|
+
method: "POST",
|
|
406
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
407
|
+
body: new URLSearchParams({
|
|
408
|
+
client_id: CLIENT_ID,
|
|
409
|
+
client_secret: CLIENT_SECRET,
|
|
410
|
+
refresh_token: credentials.refresh,
|
|
411
|
+
grant_type: "refresh_token",
|
|
412
|
+
}).toString(),
|
|
413
|
+
signal,
|
|
414
|
+
});
|
|
415
|
+
if (!response.ok) {
|
|
416
|
+
throw new Error(
|
|
417
|
+
`Antigravity token refresh failed: ${sanitizeOAuthProviderError(await response.text())}`,
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
const data = (await response.json()) as {
|
|
421
|
+
access_token: string;
|
|
422
|
+
expires_in: number;
|
|
423
|
+
refresh_token?: string;
|
|
424
|
+
};
|
|
425
|
+
// Every refresh mints a brand-new access token, so loadCodeAssist's token-keyed cache
|
|
426
|
+
// can never hit here — skip the extra round-trip entirely when we already know the
|
|
427
|
+
// project id (the normal case; it's set at login and doesn't change token to token).
|
|
428
|
+
const existingProjectId = credentialProjectId(credentials);
|
|
429
|
+
const discoveredProject = existingProjectId ? undefined : await loadCodeAssist(data.access_token);
|
|
430
|
+
const email = credentialEmail(credentials);
|
|
431
|
+
return {
|
|
432
|
+
...credentials,
|
|
433
|
+
refresh: data.refresh_token || credentials.refresh,
|
|
434
|
+
access: data.access_token,
|
|
435
|
+
expires: Date.now() + data.expires_in * 1000 - 5 * 60 * 1000,
|
|
436
|
+
projectId:
|
|
437
|
+
existingProjectId || discoveredProject || defaultProjectId(email || "antigravity-default"),
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
export function getApiKey(credentials: OAuthCredentials): string {
|
|
442
|
+
const email = credentialEmail(credentials);
|
|
443
|
+
return JSON.stringify({
|
|
444
|
+
token: credentials.access,
|
|
445
|
+
projectId: credentialProjectId(credentials) || defaultProjectId(email || "antigravity-default"),
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
export type { OAuthCredentials, OAuthLoginCallbacks } from "../pi-compat.ts";
|