@herbertgao/pi-extensions 2026.9.18 → 2026.9.19

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 (48) hide show
  1. package/README.md +19 -20
  2. package/THIRD_PARTY_NOTICES.md +1 -1
  3. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/working-message.ts +21 -6
  4. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/index.ts +15 -4
  5. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/index.ts +43 -7
  6. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/grouping.ts +7 -2
  7. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/result.ts +1 -1
  8. package/node_modules/@herbertgao/pi-cc-extensions/package.json +2 -2
  9. package/node_modules/pi-antigravity/README.md +29 -6
  10. package/node_modules/pi-antigravity/package.json +12 -7
  11. package/node_modules/pi-antigravity/src/auth/accounts.ts +327 -0
  12. package/node_modules/pi-antigravity/src/auth/index.ts +9 -0
  13. package/node_modules/pi-antigravity/src/client/client.ts +1 -1
  14. package/node_modules/pi-antigravity/src/image/image.ts +4 -4
  15. package/node_modules/pi-antigravity/src/index.ts +203 -13
  16. package/node_modules/pi-antigravity/src/search/index.ts +1 -0
  17. package/node_modules/pi-antigravity/src/search/search.ts +325 -0
  18. package/node_modules/pi-antigravity/src/stream/stream.ts +191 -14
  19. package/node_modules/pi-antigravity/src/types/enums.ts +45 -36
  20. package/node_modules/pi-antigravity/src/types/types.ts +5 -12
  21. package/node_modules/pi-antigravity/src/utils/http.ts +7 -2
  22. package/node_modules/pi-antigravity/src/utils/util.ts +31 -3
  23. package/node_modules/pi-typesafe/README.md +2 -0
  24. package/node_modules/pi-typesafe/package.json +2 -1
  25. package/node_modules/pi-web-access/CHANGELOG.md +72 -0
  26. package/node_modules/pi-web-access/README.md +34 -13
  27. package/node_modules/pi-web-access/chrome-cookies.ts +79 -25
  28. package/node_modules/pi-web-access/curator-page.ts +5 -1
  29. package/node_modules/pi-web-access/curator-server.ts +2 -0
  30. package/node_modules/pi-web-access/dist/index.js +1337 -1009
  31. package/node_modules/pi-web-access/exa.ts +20 -52
  32. package/node_modules/pi-web-access/extract.ts +41 -4
  33. package/node_modules/pi-web-access/gemini-search.ts +12 -4
  34. package/node_modules/pi-web-access/index.ts +29 -37
  35. package/node_modules/pi-web-access/openai-search.ts +35 -7
  36. package/node_modules/pi-web-access/package.json +6 -5
  37. package/node_modules/pi-web-access/page-query.ts +2 -2
  38. package/node_modules/pi-web-access/query-rewrite.ts +2 -3
  39. package/node_modules/pi-web-access/ssrf-protection.ts +27 -10
  40. package/node_modules/pi-web-access/storage.ts +48 -77
  41. package/node_modules/pi-web-access/summary-model-scope.ts +6 -77
  42. package/node_modules/pi-web-access/summary-review.ts +3 -4
  43. package/node_modules/pi-web-access/tavily.ts +67 -20
  44. package/node_modules/pi-web-access/tool-activation.ts +53 -33
  45. package/node_modules/pi-web-access/utils.ts +1 -1
  46. package/node_modules/pi-web-access/you.ts +164 -0
  47. package/node_modules/pi-web-access/zai.ts +161 -0
  48. package/package.json +6 -6
@@ -0,0 +1,327 @@
1
+ import { createHash } from "node:crypto";
2
+ import { chmodSync, existsSync, readFileSync, renameSync, writeFileSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { join } from "node:path";
5
+ import type { OAuthCredentials } from "@earendil-works/pi-ai";
6
+ import { defaultProjectId } from "../client/client.js";
7
+ import type { AntigravityApiKey, AntigravityOAuthCredentials } from "../types/types.js";
8
+ import { refreshAntigravityToken } from "./oauth.js";
9
+
10
+ const ACCOUNTS_FILE = "antigravity-accounts.json";
11
+ const AUTH_FILE = "auth.json";
12
+ const REFRESH_SKEW_MS = 5 * 60 * 1000;
13
+
14
+ export type StoredAccount = AntigravityOAuthCredentials & {
15
+ accountId: string;
16
+ addedAt: number;
17
+ lastUsedAt: number;
18
+ };
19
+
20
+ type AccountsFile = {
21
+ version: 1;
22
+ activeAccountId?: string;
23
+ accounts: Record<string, StoredAccount>;
24
+ };
25
+
26
+ export type AccountSummary = {
27
+ accountId: string;
28
+ email?: string;
29
+ active: boolean;
30
+ lastUsedAt: number;
31
+ };
32
+
33
+ function agentDir(): string {
34
+ return process.env.PI_CODING_AGENT_DIR || join(homedir(), ".pi", "agent");
35
+ }
36
+
37
+ function accountsPath(): string {
38
+ return join(agentDir(), ACCOUNTS_FILE);
39
+ }
40
+
41
+ function authPath(): string {
42
+ return join(agentDir(), AUTH_FILE);
43
+ }
44
+
45
+ function readJsonObject(path: string): Record<string, unknown> | undefined {
46
+ if (!existsSync(path)) return undefined;
47
+ try {
48
+ const parsed: unknown = JSON.parse(readFileSync(path, "utf8"));
49
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return undefined;
50
+ return parsed as Record<string, unknown>;
51
+ } catch {
52
+ return undefined;
53
+ }
54
+ }
55
+
56
+ function writePrivateJson(path: string, value: unknown): void {
57
+ const tempPath = `${path}.${process.pid}.tmp`;
58
+ writeFileSync(tempPath, `${JSON.stringify(value, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
59
+ chmodSync(tempPath, 0o600);
60
+ renameSync(tempPath, path);
61
+ chmodSync(path, 0o600);
62
+ }
63
+
64
+ function emptyStore(): AccountsFile {
65
+ return { version: 1, accounts: {} };
66
+ }
67
+
68
+ function isStoredAccount(value: unknown): value is StoredAccount {
69
+ if (!value || typeof value !== "object") return false;
70
+ const account = value as Partial<StoredAccount>;
71
+ return typeof account.refresh === "string" && account.refresh.length > 0;
72
+ }
73
+
74
+ function normalizeStore(value: Record<string, unknown> | undefined): AccountsFile {
75
+ if (!value || value.version !== 1 || !value.accounts || typeof value.accounts !== "object") {
76
+ return emptyStore();
77
+ }
78
+ const accounts: Record<string, StoredAccount> = {};
79
+ for (const [key, raw] of Object.entries(value.accounts as Record<string, unknown>)) {
80
+ if (!isStoredAccount(raw)) continue;
81
+ const accountId =
82
+ typeof raw.accountId === "string" && raw.accountId.trim() ? raw.accountId : key;
83
+ accounts[accountId] = { ...raw, accountId };
84
+ }
85
+ const activeAccountId =
86
+ typeof value.activeAccountId === "string"
87
+ ? value.activeAccountId
88
+ : typeof value.activeEmail === "string"
89
+ ? value.activeEmail
90
+ : undefined;
91
+ return {
92
+ version: 1,
93
+ activeAccountId: activeAccountId && accounts[activeAccountId] ? activeAccountId : undefined,
94
+ accounts,
95
+ };
96
+ }
97
+
98
+ export function loadAccountStore(): AccountsFile {
99
+ return normalizeStore(readJsonObject(accountsPath()));
100
+ }
101
+
102
+ function saveAccountStore(store: AccountsFile): void {
103
+ writePrivateJson(accountsPath(), store);
104
+ }
105
+
106
+ function accountIdFor(credentials: OAuthCredentials): string {
107
+ const email = (credentials as AntigravityOAuthCredentials).email?.trim().toLowerCase();
108
+ if (email) return email;
109
+ return `account-${createHash("sha256").update(credentials.refresh).digest("hex").slice(0, 16)}`;
110
+ }
111
+
112
+ function accountFromCredentials(
113
+ credentials: AntigravityOAuthCredentials,
114
+ existing?: StoredAccount,
115
+ ): StoredAccount {
116
+ const now = Date.now();
117
+ return {
118
+ ...credentials,
119
+ accountId: existing?.accountId || accountIdFor(credentials),
120
+ addedAt: existing?.addedAt || now,
121
+ lastUsedAt: now,
122
+ };
123
+ }
124
+
125
+ function sortedAccounts(store: AccountsFile): StoredAccount[] {
126
+ return Object.values(store.accounts).sort((a, b) => {
127
+ if (a.addedAt !== b.addedAt) return a.addedAt - b.addedAt;
128
+ return a.accountId.localeCompare(b.accountId);
129
+ });
130
+ }
131
+
132
+ function findAccount(
133
+ store: AccountsFile,
134
+ credentials: OAuthCredentials,
135
+ ): StoredAccount | undefined {
136
+ const email = (credentials as AntigravityOAuthCredentials).email?.trim().toLowerCase();
137
+ return Object.values(store.accounts).find(
138
+ (account) =>
139
+ account.refresh === credentials.refresh ||
140
+ (email !== undefined && account.email?.trim().toLowerCase() === email),
141
+ );
142
+ }
143
+
144
+ function findAccountId(store: AccountsFile, selector: string): string | undefined {
145
+ const normalized = selector.trim().toLowerCase();
146
+ if (!normalized) return undefined;
147
+ const entries = sortedAccounts(store);
148
+ if (/^\d+$/.test(normalized)) {
149
+ const index = Number.parseInt(normalized, 10);
150
+ if (index >= 1 && index <= entries.length) return entries[index - 1]?.accountId;
151
+ return undefined;
152
+ }
153
+ return entries.find(
154
+ (account) =>
155
+ account.accountId.toLowerCase() === normalized ||
156
+ account.email?.trim().toLowerCase() === normalized,
157
+ )?.accountId;
158
+ }
159
+
160
+ function readAuthFile(): Record<string, unknown> {
161
+ const path = authPath();
162
+ if (!existsSync(path)) return {};
163
+ const parsed = readJsonObject(path);
164
+ if (!parsed) {
165
+ throw new Error("Refusing to overwrite unreadable Pi auth.json");
166
+ }
167
+ return parsed;
168
+ }
169
+
170
+ function writeActiveCredential(account: StoredAccount | undefined): void {
171
+ const auth = readAuthFile();
172
+ if (account) {
173
+ auth.antigravity = {
174
+ type: "oauth",
175
+ email: account.email,
176
+ access: account.access,
177
+ refresh: account.refresh,
178
+ expires: account.expires,
179
+ projectId: account.projectId,
180
+ };
181
+ } else {
182
+ delete auth.antigravity;
183
+ }
184
+ writePrivateJson(authPath(), auth);
185
+ }
186
+
187
+ function currentAuthCredentials(): AntigravityOAuthCredentials | undefined {
188
+ const current = readJsonObject(authPath())?.antigravity;
189
+ if (!current || typeof current !== "object") return undefined;
190
+ const record = current as Record<string, unknown>;
191
+ if (typeof record.refresh !== "string" || !record.refresh) return undefined;
192
+ return {
193
+ access: typeof record.access === "string" ? record.access : "",
194
+ refresh: record.refresh,
195
+ expires: typeof record.expires === "number" ? record.expires : 0,
196
+ projectId: typeof record.projectId === "string" ? record.projectId : undefined,
197
+ email: typeof record.email === "string" ? record.email : undefined,
198
+ };
199
+ }
200
+
201
+ export function syncCurrentAuth(): void {
202
+ const credentials = currentAuthCredentials();
203
+ if (!credentials) return;
204
+ const store = loadAccountStore();
205
+ const existing = findAccount(store, credentials);
206
+ const account = accountFromCredentials(credentials, existing);
207
+ store.accounts[account.accountId] = account;
208
+ store.activeAccountId = account.accountId;
209
+ saveAccountStore(store);
210
+ }
211
+
212
+ export function rememberAccount(credentials: AntigravityOAuthCredentials): StoredAccount {
213
+ syncCurrentAuth();
214
+ const store = loadAccountStore();
215
+ const existing = findAccount(store, credentials);
216
+ const account = accountFromCredentials(credentials, existing);
217
+ store.accounts[account.accountId] = account;
218
+ store.activeAccountId = account.accountId;
219
+ saveAccountStore(store);
220
+ return account;
221
+ }
222
+
223
+ export function updateRememberedAccount(
224
+ previous: OAuthCredentials,
225
+ credentials: AntigravityOAuthCredentials,
226
+ ): void {
227
+ const store = loadAccountStore();
228
+ const existing = findAccount(store, previous);
229
+ if (!existing) return;
230
+ const account = accountFromCredentials(credentials, existing);
231
+ delete store.accounts[existing.accountId];
232
+ store.accounts[account.accountId] = account;
233
+ if (store.activeAccountId === existing.accountId) {
234
+ store.activeAccountId = account.accountId;
235
+ }
236
+ saveAccountStore(store);
237
+ }
238
+
239
+ export function listAccounts(): AccountSummary[] {
240
+ syncCurrentAuth();
241
+ const store = loadAccountStore();
242
+ return sortedAccounts(store).map((account) => ({
243
+ accountId: account.accountId,
244
+ email: account.email,
245
+ active: account.accountId === store.activeAccountId,
246
+ lastUsedAt: account.lastUsedAt,
247
+ }));
248
+ }
249
+
250
+ async function ensureFresh(account: StoredAccount): Promise<StoredAccount> {
251
+ if (account.access && account.expires > Date.now() + REFRESH_SKEW_MS) return account;
252
+ return accountFromCredentials(await refreshAntigravityToken(account), account);
253
+ }
254
+
255
+ function apiKeyFor(account: StoredAccount): AntigravityApiKey {
256
+ const email = account.email?.trim();
257
+ return {
258
+ token: account.access,
259
+ projectId: account.projectId || defaultProjectId(email || "antigravity-default"),
260
+ };
261
+ }
262
+
263
+ export async function activateAccount(selector: string): Promise<StoredAccount> {
264
+ syncCurrentAuth();
265
+ const store = loadAccountStore();
266
+ const accountId = findAccountId(store, selector);
267
+ if (!accountId) throw new Error(`Antigravity account not found: ${selector}`);
268
+ const existing = store.accounts[accountId];
269
+ if (!existing) throw new Error(`Antigravity account not found: ${selector}`);
270
+ const account = await ensureFresh(existing);
271
+ account.lastUsedAt = Date.now();
272
+ store.accounts[account.accountId] = account;
273
+ store.activeAccountId = account.accountId;
274
+ saveAccountStore(store);
275
+ writeActiveCredential(account);
276
+ return account;
277
+ }
278
+
279
+ export async function removeAccount(selector: string): Promise<StoredAccount | undefined> {
280
+ syncCurrentAuth();
281
+ const store = loadAccountStore();
282
+ const accountId = findAccountId(store, selector);
283
+ if (!accountId) throw new Error(`Antigravity account not found: ${selector}`);
284
+ delete store.accounts[accountId];
285
+ let next: StoredAccount | undefined;
286
+ if (store.activeAccountId === accountId) {
287
+ next = sortedAccounts(store).sort((a, b) => b.lastUsedAt - a.lastUsedAt)[0];
288
+ if (next) {
289
+ next = await ensureFresh(next);
290
+ next.lastUsedAt = Date.now();
291
+ store.accounts[next.accountId] = next;
292
+ }
293
+ store.activeAccountId = next?.accountId;
294
+ writeActiveCredential(next);
295
+ }
296
+ saveAccountStore(store);
297
+ return next;
298
+ }
299
+
300
+ /**
301
+ * Switch to the next stored account that has not already been tried in this request.
302
+ * Used when Antigravity returns a hard quota wall (HTTP 429).
303
+ */
304
+ export async function failoverToNextAccount(
305
+ triedAccessTokens: ReadonlySet<string>,
306
+ ): Promise<AntigravityApiKey | undefined> {
307
+ syncCurrentAuth();
308
+ const store = loadAccountStore();
309
+ const candidates = sortedAccounts(store).filter(
310
+ (account) => !triedAccessTokens.has(account.access),
311
+ );
312
+ for (const candidate of candidates) {
313
+ try {
314
+ const account = await ensureFresh(candidate);
315
+ if (!account.access || triedAccessTokens.has(account.access)) continue;
316
+ account.lastUsedAt = Date.now();
317
+ store.accounts[account.accountId] = account;
318
+ store.activeAccountId = account.accountId;
319
+ saveAccountStore(store);
320
+ writeActiveCredential(account);
321
+ return apiKeyFor(account);
322
+ } catch {
323
+ // Skip accounts whose refresh token is no longer valid.
324
+ }
325
+ }
326
+ return undefined;
327
+ }
@@ -12,3 +12,12 @@ export {
12
12
  OAUTH_CALLBACK_TIMEOUT_MS,
13
13
  } from "./oauth.js";
14
14
  export type { OAuthCredentials, OAuthLoginCallbacks } from "./oauth.js";
15
+ export {
16
+ activateAccount,
17
+ failoverToNextAccount,
18
+ listAccounts,
19
+ rememberAccount,
20
+ removeAccount,
21
+ updateRememberedAccount,
22
+ } from "./accounts.js";
23
+ export type { AccountSummary, StoredAccount } from "./accounts.js";
@@ -61,7 +61,7 @@ export function endpointCandidates(): string[] {
61
61
  }
62
62
 
63
63
  const DEFAULT_USER_AGENT =
64
- "antigravity/cli/1.1.23 (aidev_client; os_type=linux; arch=amd64; cl=974125021; auth_method=consumer)";
64
+ "antigravity/cli/1.2.4 (aidev_client; os_type=linux; arch=amd64; cl=982146307; auth_method=consumer)";
65
65
 
66
66
  /** Default User-Agent matching pure Antigravity CLI wire fingerprint. */
67
67
  export function defaultUserAgent(): string {
@@ -42,15 +42,15 @@ export type ImageGenerateRequest = {
42
42
  project: string;
43
43
  model: string;
44
44
  request: {
45
- contents: Array<{ role: GeminiRole.User; parts: Array<{ text: string }> }>;
46
- systemInstruction: { role: GeminiRole.User; parts: Array<{ text: string }> };
45
+ contents: Array<{ role: "user"; parts: Array<{ text: string }> }>;
46
+ systemInstruction: { role: "user"; parts: Array<{ text: string }> };
47
47
  generationConfig: {
48
48
  imageConfig: { aspectRatio: string };
49
49
  candidateCount: number;
50
50
  };
51
51
  };
52
- requestType: AntigravityRequestType.Agent;
53
- userAgent: AntigravityUserAgent.Antigravity;
52
+ requestType: "agent";
53
+ userAgent: "antigravity";
54
54
  requestId: string;
55
55
  };
56
56
 
@@ -1,8 +1,19 @@
1
1
  import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
2
2
  import { StringEnum, Type } from "@earendil-works/pi-ai";
3
- import { registerApiProvider } from "@earendil-works/pi-ai/compat";
4
- import { getApiKey, loginAntigravity, refreshAntigravityToken } from "./auth/index.js";
5
- import { DEFAULT_ENDPOINT, endpointCandidates } from "./client/index.js";
3
+ // Namespace import: Oh My Pi rewrites this specifier onto bundled pi-ai, which
4
+ // does not export registerApiProvider. A static named import fails plugin load.
5
+ import * as piAiCompat from "@earendil-works/pi-ai/compat";
6
+ import {
7
+ activateAccount,
8
+ getApiKey,
9
+ listAccounts,
10
+ loginAntigravity,
11
+ rememberAccount,
12
+ refreshAntigravityToken,
13
+ removeAccount,
14
+ updateRememberedAccount,
15
+ } from "./auth/index.js";
16
+ import { DEFAULT_ENDPOINT } from "./client/index.js";
6
17
  import { getLastDiagnostics, runWithDiagnostics } from "./diagnostics/index.js";
7
18
  import {
8
19
  DEFAULT_IMAGE_MODEL,
@@ -10,6 +21,7 @@ import {
10
21
  IMAGE_ASPECT_RATIOS,
11
22
  parseImageCommandArgs,
12
23
  } from "./image/index.js";
24
+ import { executeAntigravitySearch, parseSearchCommandArgs } from "./search/index.js";
13
25
  import {
14
26
  applyAntigravityCatalog,
15
27
  discoverAntigravityModels,
@@ -26,7 +38,7 @@ import {
26
38
  formatUsageSummary,
27
39
  resolveApiKeyFromContext,
28
40
  } from "./usage/index.js";
29
- import { prewarmConnection, redactSecrets } from "./utils/index.js";
41
+ import { redactSecrets, maskEmail } from "./utils/index.js";
30
42
 
31
43
  /**
32
44
  * Pi's interactive `notify` writes into the chat transcript. `console.log` in that
@@ -45,6 +57,22 @@ function emitCommandOutput(
45
57
  else console.log(text);
46
58
  }
47
59
 
60
+ async function loginAndRemember(
61
+ callbacks: Parameters<typeof loginAntigravity>[0],
62
+ ): ReturnType<typeof loginAntigravity> {
63
+ const credentials = await loginAntigravity(callbacks);
64
+ rememberAccount(credentials);
65
+ return credentials;
66
+ }
67
+
68
+ async function refreshAndRemember(
69
+ credentials: Parameters<typeof refreshAntigravityToken>[0],
70
+ ): ReturnType<typeof refreshAntigravityToken> {
71
+ const refreshed = await refreshAntigravityToken(credentials);
72
+ updateRememberedAccount(credentials, refreshed);
73
+ return refreshed;
74
+ }
75
+
48
76
  async function withUsage(
49
77
  ctx: ExtensionCommandContext,
50
78
  fn: (usage: Awaited<ReturnType<typeof fetchAccountUsage>>) => string,
@@ -68,17 +96,29 @@ async function withUsage(
68
96
  }
69
97
  }
70
98
 
71
- export default function (pi: ExtensionAPI): void {
72
- // Open the TLS connection up front so the first message of a session does not pay
73
- // the handshake. Opt out with ANTIGRAVITY_NO_PREWARM=1.
74
- const primaryEndpoint = endpointCandidates()[0];
75
- if (primaryEndpoint) prewarmConnection(primaryEndpoint);
99
+ type CompatApiProviderRegistrar = (provider: {
100
+ api: typeof ANTIGRAVITY_API;
101
+ stream: typeof streamAntigravity;
102
+ streamSimple: typeof streamAntigravity;
103
+ }) => void;
76
104
 
77
- registerApiProvider({
105
+ /**
106
+ * Pi dispatches custom APIs through the compat registry. Oh My Pi does not
107
+ * export `registerApiProvider` and registers the stream inside `registerProvider`.
108
+ */
109
+ function registerCompatApiProvider(): void {
110
+ const register = (piAiCompat as { registerApiProvider?: CompatApiProviderRegistrar })
111
+ .registerApiProvider;
112
+ if (typeof register !== "function") return;
113
+ register({
78
114
  api: ANTIGRAVITY_API,
79
115
  stream: streamAntigravity,
80
116
  streamSimple: streamAntigravity,
81
117
  });
118
+ }
119
+
120
+ export default function (pi: ExtensionAPI): void {
121
+ registerCompatApiProvider();
82
122
 
83
123
  const initialCatalog = getCurrentAntigravityCatalog();
84
124
 
@@ -90,8 +130,8 @@ export default function (pi: ExtensionAPI): void {
90
130
  refreshModels: refreshAntigravityModels,
91
131
  oauth: {
92
132
  name: PROVIDER_NAME,
93
- login: loginAntigravity,
94
- refreshToken: refreshAntigravityToken,
133
+ login: loginAndRemember,
134
+ refreshToken: refreshAndRemember,
95
135
  getApiKey,
96
136
  },
97
137
  streamSimple: streamAntigravity,
@@ -112,6 +152,51 @@ export default function (pi: ExtensionAPI): void {
112
152
  },
113
153
  });
114
154
 
155
+ pi.registerCommand("antigravity.accounts", {
156
+ description: "List, switch, or remove linked Antigravity Google accounts",
157
+ handler: async (args, ctx) => {
158
+ const command = args.trim();
159
+ try {
160
+ if (command.startsWith("switch ")) {
161
+ const account = await activateAccount(command.slice("switch ".length));
162
+ emitCommandOutput(
163
+ ctx,
164
+ `Active Antigravity account: ${account.email || account.accountId}`,
165
+ );
166
+ return;
167
+ }
168
+ if (command.startsWith("remove ")) {
169
+ const remaining = await removeAccount(command.slice("remove ".length));
170
+ const next = remaining
171
+ ? ` Active account is now ${remaining.email || remaining.accountId}.`
172
+ : "";
173
+ emitCommandOutput(ctx, `Antigravity account removed.${next}`);
174
+ return;
175
+ }
176
+ const accounts = listAccounts();
177
+ if (accounts.length === 0) {
178
+ emitCommandOutput(
179
+ ctx,
180
+ "No linked Antigravity accounts. Run /login antigravity to add one.",
181
+ "warning",
182
+ );
183
+ return;
184
+ }
185
+ const lines = accounts.map(
186
+ (account, index) =>
187
+ `${account.active ? "* " : " "}${index + 1}. ${account.email || account.accountId}`,
188
+ );
189
+ emitCommandOutput(
190
+ ctx,
191
+ `${lines.join("\n")}\nUse /antigravity.accounts switch <index|email> or /antigravity.accounts remove <index|email>.`,
192
+ );
193
+ } catch (error) {
194
+ const msg = error instanceof Error ? error.message : String(error);
195
+ emitCommandOutput(ctx, msg, "error");
196
+ }
197
+ },
198
+ });
199
+
115
200
  pi.registerCommand("antigravity.refresh", {
116
201
  description: "Force refresh Antigravity dynamic model catalog",
117
202
  handler: async (_args, ctx) => {
@@ -163,6 +248,12 @@ export default function (pi: ExtensionAPI): void {
163
248
  description: "Show sanitized Antigravity provider diagnostics",
164
249
  handler: async (_args, ctx) => {
165
250
  const d = getLastDiagnostics();
251
+ const accounts = listAccounts();
252
+ const active = accounts.find((account) => account.active);
253
+ const activeLabel = active
254
+ ? maskEmail(active.email) ||
255
+ (active.accountId.includes("@") ? maskEmail(active.accountId) : active.accountId)
256
+ : "none";
166
257
  const lines = [
167
258
  `provider=${PROVIDER_ID}`,
168
259
  `lastResolvedRuntimeModel=${d.resolvedRuntimeModel || "none"}`,
@@ -171,12 +262,14 @@ export default function (pi: ExtensionAPI): void {
171
262
  `lastEndpoint=${d.endpoint || "none"}`,
172
263
  `lastStatus=${d.status ?? "none"}`,
173
264
  `lastProjectId=${d.projectId || "none"}`,
265
+ `linkedAccounts=${accounts.length || "none"}`,
266
+ `activeAccount=${activeLabel || "none"}`,
174
267
  ...(d.latencyMs !== undefined ? [`lastLatencyMs=${d.latencyMs}`] : []),
175
268
  `toolSchemaWarnings=${d.toolSchemaWarnings || "none"}`,
176
269
  `lastError=${d.error ? redactSecrets(d.error) : "none"}`,
177
270
  "transport=native-streamSimple",
178
271
  "runtimeCli=not-used",
179
- "commands=/antigravity.usage /antigravity.models /antigravity.refresh /antigravity.doctor /antigravity.image",
272
+ "commands=/antigravity.usage /antigravity.models /antigravity.accounts /antigravity.refresh /antigravity.doctor /antigravity.image /antigravity.search",
180
273
  ];
181
274
  emitCommandOutput(ctx, `Antigravity doctor\n${lines.join("\n")}`);
182
275
  },
@@ -222,6 +315,44 @@ export default function (pi: ExtensionAPI): void {
222
315
  },
223
316
  });
224
317
 
318
+ pi.registerCommand("antigravity.search", {
319
+ description:
320
+ "Search the web via Antigravity Grounding (usage: /antigravity.search [--thinking] [--url <url>] <query>)",
321
+ handler: async (args, ctx) => {
322
+ const parsed = parseSearchCommandArgs(args || "");
323
+ if (!parsed.query) {
324
+ emitCommandOutput(
325
+ ctx,
326
+ "Usage: /antigravity.search [--thinking] [--url <url>] <query>",
327
+ "warning",
328
+ );
329
+ return;
330
+ }
331
+ try {
332
+ const apiKey = await resolveApiKeyFromContext(ctx);
333
+ if (!apiKey) {
334
+ emitCommandOutput(
335
+ ctx,
336
+ "No Antigravity credentials. Run /login antigravity first.",
337
+ "warning",
338
+ );
339
+ return;
340
+ }
341
+ if (ctx.hasUI) ctx.ui.notify("Searching Google via Antigravity Grounding…", "info");
342
+ const result = await executeAntigravitySearch({
343
+ apiKey,
344
+ query: parsed.query,
345
+ urls: parsed.urls,
346
+ thinking: parsed.thinking,
347
+ });
348
+ emitCommandOutput(ctx, result);
349
+ } catch (error) {
350
+ const msg = error instanceof Error ? error.message : String(error);
351
+ emitCommandOutput(ctx, `Antigravity search failed: ${redactSecrets(msg)}`, "warning");
352
+ }
353
+ },
354
+ });
355
+
225
356
  pi.registerTool({
226
357
  name: "generate_image",
227
358
  label: "Generate image",
@@ -277,4 +408,63 @@ export default function (pi: ExtensionAPI): void {
277
408
  };
278
409
  },
279
410
  });
411
+
412
+ pi.registerTool({
413
+ name: "google_search",
414
+ label: "Google Search",
415
+ description:
416
+ "Search the web using Google Search via Antigravity Grounding powered by Gemini 3 Flash. Returns real-time web results with multi-angle deep search and source citations. Use this whenever current, real-time information or web research is needed.",
417
+ promptSnippet: "Real-time Google Search grounding via Antigravity (Gemini 3 Flash)",
418
+ promptGuidelines: [
419
+ "Use google_search when you need real-time, up-to-date web information, latest news, documentation, or fact checking.",
420
+ "Pass specific URLs into the urls array if you want the search engine to fetch and analyze specific pages.",
421
+ "Set instruction to provide special research directives from the lead agent (e.g. focus on issues, compare benchmarks, restrict time range).",
422
+ ],
423
+ parameters: Type.Object({
424
+ query: Type.String({ description: "Search query or question." }),
425
+ instruction: Type.Optional(
426
+ Type.String({
427
+ description:
428
+ "Specific directive or focus from the lead agent (e.g., '重点关注英文社区近七天的讨论', 'focus on GitHub issues and benchmarks', 'ignore marketing announcements').",
429
+ }),
430
+ ),
431
+ urls: Type.Optional(
432
+ Type.Array(Type.String(), {
433
+ description: "Optional specific URLs to fetch and analyze alongside the search.",
434
+ }),
435
+ ),
436
+ thinking: Type.Optional(
437
+ Type.Boolean({
438
+ description: "Enable deeper thinking for complex analysis (default: false).",
439
+ }),
440
+ ),
441
+ }),
442
+ async execute(_toolCallId, params, signal, onUpdate, ctx) {
443
+ const apiKey = await ctx.modelRegistry.getApiKeyForProvider("antigravity");
444
+ if (!apiKey) {
445
+ throw new Error("No Antigravity credentials. Run /login antigravity first.");
446
+ }
447
+ onUpdate?.({
448
+ content: [{ type: "text", text: `Searching Google for: "${params.query}"…` }],
449
+ details: {},
450
+ });
451
+ try {
452
+ const result = await executeAntigravitySearch({
453
+ apiKey,
454
+ query: params.query,
455
+ instruction: params.instruction,
456
+ urls: params.urls,
457
+ thinking: params.thinking,
458
+ signal,
459
+ });
460
+ return {
461
+ content: [{ type: "text", text: result }],
462
+ details: {},
463
+ };
464
+ } catch (error) {
465
+ const msg = error instanceof Error ? error.message : String(error);
466
+ throw new Error(redactSecrets(msg), { cause: error });
467
+ }
468
+ },
469
+ });
280
470
  }
@@ -0,0 +1 @@
1
+ export * from "./search.js";