pi-provider-antigravity 0.8.1 → 0.9.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/README.md CHANGED
@@ -9,14 +9,16 @@ Use Google Antigravity models directly in [Pi](https://pi.dev).
9
9
 
10
10
  ## Features
11
11
 
12
- - **Verified wire compatibility**: headers, request envelopes, and tier/thinking mapping
13
- strictly validated against the Antigravity protocol specification — not guessed.
12
+ - **Wire parity**: headers, request envelopes, and thinking/tier mapping
13
+ matched to the official Antigravity client.
14
14
  - **Dynamic model catalog**: exposes exactly the models your account can use,
15
15
  synthesized from the live catalog and cached for offline startup.
16
16
  Newly released models are usable immediately without waiting for an extension update.
17
17
  - **OAuth with auto-refresh**: log in once via `/login antigravity`; tokens refresh silently.
18
18
  - **Quota visibility**: 5-hour and weekly remaining quota per model pool, with reset times
19
19
  and a live footer slot. See [Quota footer](#quota-footer).
20
+ - **Search grounding**: built-in `search_web` tool and `/antigravity search` command
21
+ with source citations (no extra API keys required).
20
22
 
21
23
  ## Requirements
22
24
 
@@ -70,6 +72,7 @@ Extension subcommands (`/antigravity ...`):
70
72
  | `models` | View available models and remaining fraction (alias: `model`) |
71
73
  | `refresh` | Fetch the latest model list from Antigravity |
72
74
  | `settings` | Configure provider settings (alias: `setting`) |
75
+ | `search` | Search the web using Google Search Grounding |
73
76
  | `login` | Shortcut: fills in `/login antigravity` for you |
74
77
 
75
78
  Related Pi builtins:
package/dist/index.js CHANGED
@@ -1,3 +1,6 @@
1
+ // src/index.ts
2
+ import { Type } from "@earendil-works/pi-ai";
3
+
1
4
  // src/auth.ts
2
5
  import { createHash, randomBytes } from "node:crypto";
3
6
 
@@ -251,8 +254,7 @@ function parseStoredCredentials(rawApiKey) {
251
254
  return { token: rawApiKey, projectId: FALLBACK_PROJECT_ID };
252
255
  }
253
256
 
254
- // src/model-catalog.ts
255
- var PRIVATE_SNAPSHOT_KEY = "pi-provider-antigravity";
257
+ // src/models.ts
256
258
  var CANONICAL_TIER_SUFFIXES = Object.freeze({
257
259
  low: "-low",
258
260
  medium: "-medium",
@@ -282,6 +284,27 @@ function extractBaseModelId(runtimeId) {
282
284
  if (runtimeId.startsWith("gemini-3.1-pro-")) return "gemini-3.1-pro";
283
285
  return runtimeId.replace(TIER_SUFFIX_PATTERN, "");
284
286
  }
287
+ var TIER_FALLBACKS = Object.freeze({
288
+ minimal: Object.freeze(["-low", "-extra-low", ""]),
289
+ low: Object.freeze(["-extra-low", ""]),
290
+ medium: Object.freeze(["", "-high"]),
291
+ // Gemini 3.1 Pro lists no -medium: up to high, never down
292
+ high: Object.freeze(["-thinking", "-agent", ""]),
293
+ // Claude's high tier is -thinking
294
+ xhigh: Object.freeze(["-high", "-thinking", "-agent", ""]),
295
+ max: Object.freeze(["-high", "-thinking", "-agent", ""])
296
+ });
297
+ var DEFAULT_TIER_ORDER = Object.freeze([
298
+ CANONICAL_TIER_SUFFIXES.high,
299
+ ...TIER_ALIASES.high,
300
+ ""
301
+ ]);
302
+ function tierCandidateOrder(effort) {
303
+ if (!effort) return DEFAULT_TIER_ORDER;
304
+ const canonical = effort in CANONICAL_TIER_SUFFIXES ? [CANONICAL_TIER_SUFFIXES[effort]] : [`-${effort}`];
305
+ const fallbacks = TIER_FALLBACKS[effort] ?? DEFAULT_TIER_ORDER;
306
+ return [.../* @__PURE__ */ new Set([...canonical, ...fallbacks])];
307
+ }
285
308
  function classifyModelFamily(modelId) {
286
309
  const bare = ((modelId || "").split("/").pop() || "").toLowerCase();
287
310
  if (bare.startsWith("gemini-")) return "gemini";
@@ -296,6 +319,9 @@ function isCompatibleFamily(msgModel, targetModelId) {
296
319
  if (msgFamily !== "unknown" && msgFamily === classifyModelFamily(targetModelId)) return true;
297
320
  return extractBaseModelId(msgModel) === extractBaseModelId(targetModelId);
298
321
  }
322
+
323
+ // src/model-catalog.ts
324
+ var PRIVATE_SNAPSHOT_KEY = "pi-provider-antigravity";
299
325
  function buildThinkingMap(models) {
300
326
  return Object.fromEntries(
301
327
  models.map((m) => [
@@ -326,62 +352,10 @@ function copySnapshot(snapshot) {
326
352
  };
327
353
  }
328
354
  var EMPTY_SNAPSHOT = () => ({ enums: {}, runtimeIds: [], thinking: {}, deprecated: {} });
329
- function createModelCatalog() {
330
- let generation = { snapshot: EMPTY_SNAPSHOT(), version: 0 };
331
- const record = (catalog) => {
332
- generation = {
333
- snapshot: snapshotFromCatalog(catalog),
334
- items: catalog,
335
- version: generation.version + 1
336
- };
337
- };
338
- const restore = (snapshot) => {
339
- if (generation.version > 0) return;
340
- generation = { snapshot: copySnapshot(snapshot), version: 0 };
341
- };
342
- const refresh = async (context) => {
343
- const persisted = fromPersistedSnapshot(context.stored?.[PRIVATE_SNAPSHOT_KEY]);
344
- if (persisted) restore(persisted);
345
- const storedModels = () => [...context.stored?.models ?? []];
346
- if (!context.allowNetwork) return storedModels();
347
- try {
348
- const creds = await resolveCredentials(context);
349
- if (!creds) return storedModels();
350
- const catalog = await fetchAvailableModelsCatalog(creds, creds.projectId, context.signal);
351
- record(catalog);
352
- const dynamicModels = buildDynamicPublicModels(catalog);
353
- if (context.publish) {
354
- await context.publish({
355
- persist: {
356
- models: dynamicModels,
357
- checkedAt: Date.now(),
358
- [PRIVATE_SNAPSHOT_KEY]: toPersistedSnapshot(generation.snapshot)
359
- }
360
- });
361
- }
362
- return dynamicModels;
363
- } catch {
364
- return storedModels();
365
- }
366
- };
367
- const refreshGen = async (doRefresh) => {
368
- const versionBefore = generation.version;
369
- await doRefresh();
370
- const { items, version } = generation;
371
- if (!items) return { status: "failed" };
372
- return { status: version === versionBefore ? "stale" : "fresh", catalog: items };
373
- };
374
- return {
375
- generation: () => ({ ...generation, snapshot: copySnapshot(generation.snapshot) }),
376
- record,
377
- restore,
378
- refresh,
379
- refreshGeneration: refreshGen,
380
- resolvePlan: (publicModelId, effort) => resolveModelPlan(publicModelId, effort, generation.snapshot),
381
- formatList: () => generation.items ? formatModelsList(generation.items) : "No models available."
382
- };
383
- }
384
355
  function parseAvailableModels(data) {
356
+ if (!data || typeof data !== "object") {
357
+ return { models: [], modelEnums: {} };
358
+ }
385
359
  const models = [];
386
360
  const modelEnums = {};
387
361
  const modelsObj = data.models || {};
@@ -468,27 +442,6 @@ function fromPersistedSnapshot(raw) {
468
442
  if (!enums && !runtimeIds && !thinking && !deprecated) return void 0;
469
443
  return { enums: enums ?? {}, runtimeIds: runtimeIds ?? [], thinking: thinking ?? {}, deprecated: deprecated ?? {} };
470
444
  }
471
- var TIER_FALLBACKS = Object.freeze({
472
- minimal: Object.freeze(["-low", "-extra-low", ""]),
473
- low: Object.freeze(["-extra-low", ""]),
474
- medium: Object.freeze(["", "-high"]),
475
- // Gemini 3.1 Pro lists no -medium: up to high, never down
476
- high: Object.freeze(["-thinking", "-agent", ""]),
477
- // Claude's high tier is -thinking
478
- xhigh: Object.freeze(["-high", "-thinking", "-agent", ""]),
479
- max: Object.freeze(["-high", "-thinking", "-agent", ""])
480
- });
481
- var DEFAULT_TIER_ORDER = Object.freeze([
482
- CANONICAL_TIER_SUFFIXES.high,
483
- ...TIER_ALIASES.high,
484
- ""
485
- ]);
486
- function tierCandidateOrder(effort) {
487
- if (!effort) return DEFAULT_TIER_ORDER;
488
- const canonical = effort in CANONICAL_TIER_SUFFIXES ? [CANONICAL_TIER_SUFFIXES[effort]] : [`-${effort}`];
489
- const fallbacks = TIER_FALLBACKS[effort] ?? DEFAULT_TIER_ORDER;
490
- return [.../* @__PURE__ */ new Set([...canonical, ...fallbacks])];
491
- }
492
445
  function resolveRuntimeModelId(modelId, effort, availableRuntimeIds) {
493
446
  if (ALL_TIER_SUFFIXES.some((suffix) => modelId.endsWith(suffix))) {
494
447
  return modelId;
@@ -507,6 +460,21 @@ function resolveRuntimeModelId(modelId, effort, availableRuntimeIds) {
507
460
  }
508
461
  return modelId;
509
462
  }
463
+ function followRenames(runtimeModelId, snapshot) {
464
+ const renamed = snapshot.deprecated;
465
+ if (!renamed) return runtimeModelId;
466
+ let current = runtimeModelId;
467
+ const seen = /* @__PURE__ */ new Set([current]);
468
+ while (renamed[current] && !seen.has(renamed[current])) {
469
+ current = renamed[current];
470
+ seen.add(current);
471
+ }
472
+ return current;
473
+ }
474
+ function resolveThinkingConfig(runtimeModelId, snapshot) {
475
+ const budget = snapshot.thinking[runtimeModelId]?.budget;
476
+ return typeof budget === "number" ? { includeThoughts: true, thinkingBudget: budget } : { includeThoughts: false, thinkingBudget: 0 };
477
+ }
510
478
  function resolveModelPlan(publicModelId, effort, snapshot) {
511
479
  const runtimeModelId = followRenames(
512
480
  resolveRuntimeModelId(publicModelId, effort, snapshot.runtimeIds),
@@ -527,24 +495,22 @@ function resolveModelPlan(publicModelId, effort, snapshot) {
527
495
  isClaude: family === "claude"
528
496
  };
529
497
  }
530
- function followRenames(runtimeModelId, snapshot) {
531
- const renamed = snapshot.deprecated;
532
- if (!renamed) return runtimeModelId;
533
- let current = runtimeModelId;
534
- const seen = /* @__PURE__ */ new Set([current]);
535
- while (renamed[current] && !seen.has(renamed[current])) {
536
- current = renamed[current];
537
- seen.add(current);
538
- }
539
- return current;
540
- }
541
- function resolveThinkingConfig(runtimeModelId, snapshot) {
542
- const budget = snapshot.thinking[runtimeModelId]?.budget;
543
- return typeof budget === "number" ? { includeThoughts: true, thinkingBudget: budget } : { includeThoughts: false, thinkingBudget: 0 };
544
- }
545
498
  function estimateModelCost(_baseId) {
546
499
  return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
547
500
  }
501
+ function formatModelDisplayName(baseId, rawDisplayName) {
502
+ if (rawDisplayName) {
503
+ const cleaned = rawDisplayName.replace(/\s*\([^)]*\)/g, "").trim();
504
+ if (cleaned.length > 0) {
505
+ return cleaned;
506
+ }
507
+ }
508
+ const words = baseId.split("-").map((w) => {
509
+ if (/^\d+(\.\d+)?$/.test(w)) return w;
510
+ return w.charAt(0).toUpperCase() + w.slice(1);
511
+ });
512
+ return words.join(" ");
513
+ }
548
514
  function synthesizeDynamicModel(baseId, items) {
549
515
  const repItem = items.find((it) => it.id === `${baseId}-high` || it.id === baseId) || items[0];
550
516
  const hasVariant = (suffixes) => items.some((it) => suffixes.some((suffix) => it.id.endsWith(suffix)));
@@ -604,19 +570,6 @@ function buildDynamicPublicModels(catalog) {
604
570
  }
605
571
  return orderedBaseIds.map((baseId) => synthesizeDynamicModel(baseId, runtimeGroups.get(baseId)));
606
572
  }
607
- function formatModelDisplayName(baseId, rawDisplayName) {
608
- if (rawDisplayName) {
609
- const cleaned = rawDisplayName.replace(/\s*\([^)]*\)/g, "").trim();
610
- if (cleaned.length > 0) {
611
- return cleaned;
612
- }
613
- }
614
- const words = baseId.split("-").map((w) => {
615
- if (/^\d+(\.\d+)?$/.test(w)) return w;
616
- return w.charAt(0).toUpperCase() + w.slice(1);
617
- });
618
- return words.join(" ");
619
- }
620
573
  function formatTokenCount(n) {
621
574
  if (typeof n !== "number") return "N/A";
622
575
  if (n % 1048576 === 0) return `${n / 1048576}M`;
@@ -661,6 +614,82 @@ function formatModelsList(catalog) {
661
614
  }
662
615
  return lines.join("\n");
663
616
  }
617
+ function createModelCatalog() {
618
+ let generation = { snapshot: EMPTY_SNAPSHOT(), version: 0 };
619
+ const record = (catalogOrRaw) => {
620
+ let catalog;
621
+ if (catalogOrRaw && typeof catalogOrRaw === "object" && "models" in catalogOrRaw && Array.isArray(catalogOrRaw.models) && "modelEnums" in catalogOrRaw && typeof catalogOrRaw.modelEnums === "object") {
622
+ catalog = catalogOrRaw;
623
+ } else {
624
+ catalog = parseAvailableModels(catalogOrRaw);
625
+ }
626
+ generation = {
627
+ snapshot: snapshotFromCatalog(catalog),
628
+ items: catalog,
629
+ version: generation.version + 1
630
+ };
631
+ };
632
+ const restore = (persistedOrSnapshot) => {
633
+ if (generation.version > 0) return;
634
+ if (!persistedOrSnapshot || typeof persistedOrSnapshot !== "object") return;
635
+ if ("enums" in persistedOrSnapshot && "runtimeIds" in persistedOrSnapshot && Array.isArray(persistedOrSnapshot.runtimeIds)) {
636
+ generation = { snapshot: copySnapshot(persistedOrSnapshot), version: 0 };
637
+ return;
638
+ }
639
+ const snapshot = fromPersistedSnapshot(persistedOrSnapshot);
640
+ if (snapshot) {
641
+ generation = { snapshot: copySnapshot(snapshot), version: 0 };
642
+ }
643
+ };
644
+ const toPersisted = () => {
645
+ if (generation.version === 0 && generation.snapshot.runtimeIds.length === 0) {
646
+ return void 0;
647
+ }
648
+ return toPersistedSnapshot(generation.snapshot);
649
+ };
650
+ const refresh = async (context) => {
651
+ const persisted = context.stored?.[PRIVATE_SNAPSHOT_KEY];
652
+ if (persisted) restore(persisted);
653
+ const storedModels = () => [...context.stored?.models ?? []];
654
+ if (!context.allowNetwork) return storedModels();
655
+ try {
656
+ const creds = await resolveCredentials(context);
657
+ if (!creds) return storedModels();
658
+ const catalog = await fetchAvailableModelsCatalog(creds, creds.projectId, context.signal);
659
+ record(catalog);
660
+ const dynamicModels = buildDynamicPublicModels(catalog);
661
+ if (context.publish) {
662
+ await context.publish({
663
+ persist: {
664
+ models: dynamicModels,
665
+ checkedAt: Date.now(),
666
+ [PRIVATE_SNAPSHOT_KEY]: toPersisted()
667
+ }
668
+ });
669
+ }
670
+ return dynamicModels;
671
+ } catch {
672
+ return storedModels();
673
+ }
674
+ };
675
+ const refreshGen = async (doRefresh) => {
676
+ const versionBefore = generation.version;
677
+ await doRefresh();
678
+ const { items, version } = generation;
679
+ if (!items) return { status: "failed" };
680
+ return { status: version === versionBefore ? "stale" : "fresh", catalog: items };
681
+ };
682
+ return {
683
+ generation: () => ({ ...generation, snapshot: copySnapshot(generation.snapshot) }),
684
+ record,
685
+ restore,
686
+ toPersisted,
687
+ refresh,
688
+ refreshGeneration: refreshGen,
689
+ resolvePlan: (publicModelId, effort) => resolveModelPlan(publicModelId, effort, generation.snapshot),
690
+ formatList: () => generation.items ? formatModelsList(generation.items) : "No models available."
691
+ };
692
+ }
664
693
 
665
694
  // src/quota-status.ts
666
695
  import * as fs from "node:fs";
@@ -1876,6 +1905,102 @@ function streamAntigravity(model, context, options, catalog) {
1876
1905
  return stream;
1877
1906
  }
1878
1907
 
1908
+ // src/search.ts
1909
+ var SEARCH_SYSTEM_INSTRUCTION = "You are a search engine bot. You will be given a query from a user. Your task is to search the web for relevant information that will help the user. You MUST perform a web search. Do not respond or interact with the user, please respond as if they typed the query into a search bar.";
1910
+ var SEARCH_MODEL = "gemini-3.1-flash-lite";
1911
+ function formatSearchResults(rawText, metadata, query, createdAt, completedAt) {
1912
+ const sources = (metadata?.groundingChunks || []).map((chunk, i) => ({
1913
+ index: i + 1,
1914
+ title: chunk.web?.title || "Web Source",
1915
+ url: chunk.web?.uri || ""
1916
+ })).filter((s) => Boolean(s.url));
1917
+ const queries = metadata?.webSearchQueries || [];
1918
+ let annotatedText = rawText;
1919
+ const supports = (metadata?.groundingSupports || []).filter(
1920
+ (s) => typeof s.segment?.endIndex === "number" && Array.isArray(s.groundingChunkIndices) && s.groundingChunkIndices.length > 0
1921
+ ).slice().sort((a, b) => (b.segment.endIndex ?? 0) - (a.segment.endIndex ?? 0));
1922
+ for (const sup of supports) {
1923
+ const end = sup.segment.endIndex;
1924
+ if (end >= 0 && end <= annotatedText.length) {
1925
+ const citations = (sup.groundingChunkIndices || []).map((idx) => `[${idx + 1}]`).join("");
1926
+ annotatedText = annotatedText.slice(0, end) + citations + annotatedText.slice(end);
1927
+ }
1928
+ }
1929
+ const sourceLines = sources.map((s) => `[${s.index}] [${s.title}](${s.url})`);
1930
+ let formattedOutput = `Created At: ${createdAt}
1931
+ Completed At: ${completedAt}
1932
+ The search for "${query}" returned the following summary:
1933
+ ${annotatedText.trim() || "No content returned."}`;
1934
+ if (sourceLines.length > 0) {
1935
+ formattedOutput += `
1936
+
1937
+ Sources:
1938
+ ${sourceLines.join("\n")}`;
1939
+ }
1940
+ return {
1941
+ query,
1942
+ rawText,
1943
+ formattedOutput,
1944
+ sources,
1945
+ queries,
1946
+ createdAt,
1947
+ completedAt
1948
+ };
1949
+ }
1950
+ async function performWebSearch(creds, options) {
1951
+ const createdAt = (/* @__PURE__ */ new Date()).toISOString();
1952
+ let effectiveQuery = options.query.trim();
1953
+ if (options.domain && options.domain.trim()) {
1954
+ const d = options.domain.trim();
1955
+ if (!effectiveQuery.toLowerCase().includes(`site:${d.toLowerCase()}`)) {
1956
+ effectiveQuery = `${effectiveQuery} site:${d}`;
1957
+ }
1958
+ }
1959
+ const reqBody = {
1960
+ project: creds.projectId || FALLBACK_PROJECT_ID,
1961
+ request: {
1962
+ contents: [
1963
+ {
1964
+ role: "user",
1965
+ parts: [{ text: effectiveQuery }]
1966
+ }
1967
+ ],
1968
+ systemInstruction: {
1969
+ role: "user",
1970
+ parts: [{ text: SEARCH_SYSTEM_INSTRUCTION }]
1971
+ },
1972
+ tools: [
1973
+ {
1974
+ googleSearch: {
1975
+ enhancedContent: {
1976
+ imageSearch: {
1977
+ maxResultCount: 5
1978
+ }
1979
+ }
1980
+ }
1981
+ }
1982
+ ],
1983
+ generationConfig: {
1984
+ candidateCount: 1
1985
+ }
1986
+ },
1987
+ model: SEARCH_MODEL,
1988
+ userAgent: "antigravity",
1989
+ requestType: "web_search"
1990
+ };
1991
+ const resp = await postAntigravityJson({
1992
+ auth: creds.token,
1993
+ path: "v1internal:generateContent",
1994
+ body: reqBody,
1995
+ signal: options.signal
1996
+ });
1997
+ const completedAt = (/* @__PURE__ */ new Date()).toISOString();
1998
+ const candidate = resp.response?.candidates?.[0];
1999
+ const rawText = candidate?.content?.parts?.filter((p) => typeof p.text === "string").map((p) => p.text).join("") || "";
2000
+ const metadata = candidate?.groundingMetadata;
2001
+ return formatSearchResults(rawText, metadata, effectiveQuery, createdAt, completedAt);
2002
+ }
2003
+
1879
2004
  // src/commands.ts
1880
2005
  function emitOutput(ctx, text, type = "info") {
1881
2006
  if (ctx.hasUI) {
@@ -1908,6 +2033,11 @@ var SUBCOMMANDS = Object.freeze([
1908
2033
  aliases: Object.freeze(["setting"]),
1909
2034
  run: async ({ ctx, quotaStatus }) => openSettings(ctx, quotaStatus)
1910
2035
  },
2036
+ {
2037
+ name: "search",
2038
+ description: "Search the web using Google Search Grounding",
2039
+ run: async ({ ctx, subArgs }) => runSearchSubcommand(ctx, subArgs)
2040
+ },
1911
2041
  {
1912
2042
  name: "login",
1913
2043
  description: "Run /login antigravity",
@@ -2013,12 +2143,36 @@ function runLoginSubcommand(ctx) {
2013
2143
  emitOutput(ctx, "Please run /login antigravity to authenticate.");
2014
2144
  }
2015
2145
  }
2146
+ async function runSearchSubcommand(ctx, query) {
2147
+ const creds = await resolveCredentials(ctx);
2148
+ if (!creds) {
2149
+ emitOutput(ctx, NOT_LOGGED_IN, "warning");
2150
+ return;
2151
+ }
2152
+ const trimmedQuery = (query || "").trim();
2153
+ if (!trimmedQuery) {
2154
+ emitOutput(ctx, "Usage: /antigravity search <query>", "warning");
2155
+ return;
2156
+ }
2157
+ try {
2158
+ if (ctx.hasUI) ctx.ui.notify(`Searching: "${trimmedQuery}"\u2026`, "info");
2159
+ const result = await performWebSearch(creds, {
2160
+ query: trimmedQuery,
2161
+ signal: ctx.signal
2162
+ });
2163
+ emitOutput(ctx, result.formattedOutput);
2164
+ } catch (err) {
2165
+ emitOutput(ctx, `Search failed: ${err.message}`, "error");
2166
+ }
2167
+ }
2016
2168
  async function runAntigravitySubcommand(args, ctx, quotaStatus, catalog) {
2017
- const parts = (args || "").trim().split(/\s+/).filter(Boolean);
2169
+ const raw = (args || "").trim();
2170
+ const parts = raw.split(/\s+/).filter(Boolean);
2018
2171
  const sub = parseAntigravitySubcommand(parts[0] || "");
2172
+ const subArgs = raw.slice(parts[0]?.length || 0).trim();
2019
2173
  const cmd = SUBCOMMANDS.find((c) => c.name === sub);
2020
2174
  if (cmd) {
2021
- await cmd.run({ ctx, quotaStatus, catalog });
2175
+ await cmd.run({ ctx, quotaStatus, catalog, subArgs });
2022
2176
  return;
2023
2177
  }
2024
2178
  emitOutput(ctx, USAGE_TEXT);
@@ -2051,6 +2205,53 @@ function index_default(pi) {
2051
2205
  await runAntigravitySubcommand(args, ctx, quotaStatus, catalog);
2052
2206
  }
2053
2207
  });
2208
+ if (typeof pi.registerTool === "function") {
2209
+ pi.registerTool({
2210
+ name: "search_web",
2211
+ label: "Search Web",
2212
+ description: "Performs a web search for a given query. Returns a summary of relevant information along with URL citations.",
2213
+ promptSnippet: "Search the web using Google Search Grounding with source citations",
2214
+ promptGuidelines: [
2215
+ "Use search_web when you need real-time web search, latest documentation, or up-to-date facts with verified citations."
2216
+ ],
2217
+ parameters: Type.Object({
2218
+ query: Type.String({ description: "The search query string to look up on the web." }),
2219
+ domain: Type.Optional(
2220
+ Type.String({
2221
+ description: "Optional domain to recommend the search prioritize (e.g. 'github.com', 'developer.mozilla.org')."
2222
+ })
2223
+ )
2224
+ }),
2225
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
2226
+ const creds = await resolveCredentials(ctx);
2227
+ if (!creds) {
2228
+ return {
2229
+ content: [{ type: "text", text: "Error: Not logged in. Run /login antigravity first." }],
2230
+ details: { error: "not_logged_in" }
2231
+ };
2232
+ }
2233
+ try {
2234
+ const result = await performWebSearch(creds, {
2235
+ query: params.query,
2236
+ domain: params.domain,
2237
+ signal
2238
+ });
2239
+ return {
2240
+ content: [{ type: "text", text: result.formattedOutput }],
2241
+ details: {
2242
+ sources: result.sources,
2243
+ queries: result.queries
2244
+ }
2245
+ };
2246
+ } catch (err) {
2247
+ return {
2248
+ content: [{ type: "text", text: `Search failed: ${err.message}` }],
2249
+ details: { error: err.message }
2250
+ };
2251
+ }
2252
+ }
2253
+ });
2254
+ }
2054
2255
  pi.on("session_start", async (_event, ctx) => {
2055
2256
  await quotaStatus.refreshAndPaint(ctx);
2056
2257
  });