@f5-sales-demo/pi-ai 20.3.2 → 20.3.3

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@f5-sales-demo/pi-ai",
4
- "version": "20.3.2",
4
+ "version": "20.3.3",
5
5
  "description": "Unified LLM API with automatic model discovery and provider configuration",
6
6
  "homepage": "https://github.com/f5-sales-demo/xcsh",
7
7
  "author": "Can Boluk",
@@ -46,7 +46,7 @@
46
46
  "@anthropic-ai/sdk": "^0.115",
47
47
  "@aws-sdk/client-bedrock-runtime": "^3",
48
48
  "@bufbuild/protobuf": "^2.11",
49
- "@f5-sales-demo/pi-utils": "20.3.2",
49
+ "@f5-sales-demo/pi-utils": "20.3.3",
50
50
  "@google/genai": "^2.15",
51
51
  "@sinclair/typebox": "^0.34",
52
52
  "@smithy/node-http-handler": "^4.4",
@@ -2,6 +2,9 @@
2
2
  * Antigravity OAuth flow (Gemini 3, Claude, GPT-OSS via Google Cloud)
3
3
  * Uses different OAuth credentials than google-gemini-cli for access to additional models.
4
4
  */
5
+ import { readFile } from "node:fs/promises";
6
+ import { homedir } from "node:os";
7
+ import { join } from "node:path";
5
8
  import { $env } from "@f5-sales-demo/pi-utils";
6
9
  import { getAntigravityAuthHeaders } from "../../providers/google-gemini-cli";
7
10
  import { OAuthCallbackFlow } from "./callback-server";
@@ -29,6 +32,9 @@ const CLOUD_CODE_ENDPOINT = "https://cloudcode-pa.googleapis.com";
29
32
  const TIER_LEGACY = "legacy-tier";
30
33
  const PROJECT_ONBOARD_MAX_ATTEMPTS = 5;
31
34
  const PROJECT_ONBOARD_INTERVAL_MS = 2000;
35
+ const PROJECT_ID_PATTERN = /^[a-z][a-z0-9-]{4,28}[a-z0-9]$/;
36
+ const PROJECT_NUMBER_PATTERN = /^\d{12}$/;
37
+ const ANTIGRAVITY_CLI_TOKEN_PATH = join(homedir(), ".gemini", "antigravity-cli", "antigravity-oauth-token");
32
38
 
33
39
  interface LoadCodeAssistPayload {
34
40
  cloudaicompanionProject?: string | { id?: string };
@@ -54,6 +60,121 @@ interface AntigravityProjectRequest {
54
60
  metadata: typeof ANTIGRAVITY_LOAD_CODE_ASSIST_METADATA & { duetProject?: string };
55
61
  }
56
62
 
63
+ interface ProjectEnvironment {
64
+ GOOGLE_CLOUD_PROJECT?: string;
65
+ GOOGLE_CLOUD_PROJECT_ID?: string;
66
+ }
67
+
68
+ interface GcloudProjectCommandResult {
69
+ exitCode: number;
70
+ stdout: string;
71
+ }
72
+
73
+ type GcloudProjectCommand = () => Promise<GcloudProjectCommandResult>;
74
+
75
+ export interface AntigravityProjectSources {
76
+ environment?: ProjectEnvironment;
77
+ readAntigravityProjectId?: () => Promise<string | undefined>;
78
+ readGcloudProjectId?: () => Promise<string | undefined>;
79
+ }
80
+
81
+ export interface AntigravityLoginOptions {
82
+ projectSources?: AntigravityProjectSources;
83
+ }
84
+
85
+ function normalizeProjectId(value: unknown): string | undefined {
86
+ if (typeof value !== "string") return undefined;
87
+ const projectId = value.trim();
88
+ return PROJECT_ID_PATTERN.test(projectId) || PROJECT_NUMBER_PATTERN.test(projectId) ? projectId : undefined;
89
+ }
90
+
91
+ export async function readAntigravityCliProjectId(
92
+ metadataPath = ANTIGRAVITY_CLI_TOKEN_PATH,
93
+ ): Promise<string | undefined> {
94
+ try {
95
+ const metadata = JSON.parse(await readFile(metadataPath, "utf8")) as unknown;
96
+ if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) return undefined;
97
+ return normalizeProjectId((metadata as { project_id?: unknown }).project_id);
98
+ } catch {
99
+ return undefined;
100
+ }
101
+ }
102
+
103
+ async function runGcloudProjectCommand(): Promise<GcloudProjectCommandResult> {
104
+ const subprocess = Bun.spawn(["gcloud", "config", "get-value", "project"], {
105
+ stdin: "ignore",
106
+ stdout: "pipe",
107
+ stderr: "ignore",
108
+ });
109
+ const [stdout, exitCode] = await Promise.all([new Response(subprocess.stdout).text(), subprocess.exited]);
110
+ return { exitCode, stdout };
111
+ }
112
+
113
+ export async function readGcloudProjectId(
114
+ runCommand: GcloudProjectCommand = runGcloudProjectCommand,
115
+ ): Promise<string | undefined> {
116
+ try {
117
+ const result = await runCommand();
118
+ if (result.exitCode !== 0) return undefined;
119
+ return normalizeProjectId(result.stdout);
120
+ } catch {
121
+ return undefined;
122
+ }
123
+ }
124
+
125
+ async function readExternalProjectId(source: () => Promise<string | undefined>): Promise<string | undefined> {
126
+ try {
127
+ return normalizeProjectId(await source());
128
+ } catch {
129
+ return undefined;
130
+ }
131
+ }
132
+
133
+ export async function resolveAntigravityProjectId(
134
+ ctrl: OAuthController,
135
+ sources: AntigravityProjectSources = {},
136
+ ): Promise<string | undefined> {
137
+ const environment = sources.environment ?? {
138
+ GOOGLE_CLOUD_PROJECT: $env.GOOGLE_CLOUD_PROJECT,
139
+ GOOGLE_CLOUD_PROJECT_ID: $env.GOOGLE_CLOUD_PROJECT_ID,
140
+ };
141
+ const environmentProjectId =
142
+ normalizeProjectId(environment.GOOGLE_CLOUD_PROJECT) ?? normalizeProjectId(environment.GOOGLE_CLOUD_PROJECT_ID);
143
+ if (environmentProjectId) {
144
+ ctrl.onProgress?.("Using the Google Cloud project configured by the environment...");
145
+ return environmentProjectId;
146
+ }
147
+
148
+ const antigravityProjectId = await readExternalProjectId(
149
+ sources.readAntigravityProjectId ?? readAntigravityCliProjectId,
150
+ );
151
+ if (antigravityProjectId) {
152
+ ctrl.onProgress?.("Using the Google Cloud project configured by Antigravity CLI...");
153
+ return antigravityProjectId;
154
+ }
155
+
156
+ const gcloudProjectId = await readExternalProjectId(sources.readGcloudProjectId ?? readGcloudProjectId);
157
+ if (gcloudProjectId) {
158
+ ctrl.onProgress?.("Using the Google Cloud project configured by gcloud...");
159
+ return gcloudProjectId;
160
+ }
161
+
162
+ if (!ctrl.onPrompt) return undefined;
163
+ const input = await ctrl.onPrompt({
164
+ message: "Google Cloud project ID (leave blank to use individual-tier discovery):",
165
+ placeholder: "my-enterprise-project",
166
+ allowEmpty: true,
167
+ });
168
+ if (!input.trim()) return undefined;
169
+ const promptedProjectId = normalizeProjectId(input);
170
+ if (!promptedProjectId) {
171
+ throw new Error(
172
+ "Invalid Google Cloud project. Use a 6-30 character lowercase project ID or a 12-digit project number.",
173
+ );
174
+ }
175
+ return promptedProjectId;
176
+ }
177
+
57
178
  function buildProjectRequest(configuredProjectId: string | undefined): AntigravityProjectRequest {
58
179
  if (!configuredProjectId) {
59
180
  return { metadata: ANTIGRAVITY_LOAD_CODE_ASSIST_METADATA };
@@ -127,8 +248,11 @@ async function onboardProjectWithRetries(
127
248
  );
128
249
  }
129
250
 
130
- async function discoverProject(accessToken: string, onProgress?: (message: string) => void): Promise<string> {
131
- const configuredProjectId = $env.GOOGLE_CLOUD_PROJECT || $env.GOOGLE_CLOUD_PROJECT_ID;
251
+ async function discoverProject(
252
+ accessToken: string,
253
+ configuredProjectId: string | undefined,
254
+ onProgress?: (message: string) => void,
255
+ ): Promise<string> {
132
256
  const projectRequest = buildProjectRequest(configuredProjectId);
133
257
  const headers = {
134
258
  Authorization: `Bearer ${accessToken}`,
@@ -188,8 +312,11 @@ async function getUserEmail(accessToken: string): Promise<string | undefined> {
188
312
  }
189
313
 
190
314
  class AntigravityOAuthFlow extends OAuthCallbackFlow {
191
- constructor(ctrl: OAuthController) {
315
+ #configuredProjectId: string | undefined;
316
+
317
+ constructor(ctrl: OAuthController, configuredProjectId: string | undefined) {
192
318
  super(ctrl, CALLBACK_PORT, CALLBACK_PATH);
319
+ this.#configuredProjectId = configuredProjectId;
193
320
  }
194
321
 
195
322
  async generateAuthUrl(state: string, redirectUri: string): Promise<{ url: string; instructions?: string }> {
@@ -239,7 +366,7 @@ class AntigravityOAuthFlow extends OAuthCallbackFlow {
239
366
 
240
367
  this.ctrl.onProgress?.("Getting user info...");
241
368
  const email = await getUserEmail(tokenData.access_token);
242
- const projectId = await discoverProject(tokenData.access_token, this.ctrl.onProgress);
369
+ const projectId = await discoverProject(tokenData.access_token, this.#configuredProjectId, this.ctrl.onProgress);
243
370
 
244
371
  return {
245
372
  refresh: tokenData.refresh_token,
@@ -254,8 +381,12 @@ class AntigravityOAuthFlow extends OAuthCallbackFlow {
254
381
  /**
255
382
  * Login with Antigravity OAuth
256
383
  */
257
- export async function loginAntigravity(ctrl: OAuthController): Promise<OAuthCredentials> {
258
- const flow = new AntigravityOAuthFlow(ctrl);
384
+ export async function loginAntigravity(
385
+ ctrl: OAuthController,
386
+ options: AntigravityLoginOptions = {},
387
+ ): Promise<OAuthCredentials> {
388
+ const configuredProjectId = await resolveAntigravityProjectId(ctrl, options.projectSources);
389
+ const flow = new AntigravityOAuthFlow(ctrl, configuredProjectId);
259
390
  return flow.login();
260
391
  }
261
392