@f5-sales-demo/pi-ai 20.3.1 → 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.1",
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.1",
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,10 @@
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";
8
+ import { $env } from "@f5-sales-demo/pi-utils";
5
9
  import { getAntigravityAuthHeaders } from "../../providers/google-gemini-cli";
6
10
  import { OAuthCallbackFlow } from "./callback-server";
7
11
  import type { OAuthController, OAuthCredentials } from "./types";
@@ -28,6 +32,9 @@ const CLOUD_CODE_ENDPOINT = "https://cloudcode-pa.googleapis.com";
28
32
  const TIER_LEGACY = "legacy-tier";
29
33
  const PROJECT_ONBOARD_MAX_ATTEMPTS = 5;
30
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");
31
38
 
32
39
  interface LoadCodeAssistPayload {
33
40
  cloudaicompanionProject?: string | { id?: string };
@@ -48,6 +55,139 @@ export const ANTIGRAVITY_LOAD_CODE_ASSIST_METADATA = Object.freeze({
48
55
  pluginType: "GEMINI",
49
56
  });
50
57
 
58
+ interface AntigravityProjectRequest {
59
+ cloudaicompanionProject?: string;
60
+ metadata: typeof ANTIGRAVITY_LOAD_CODE_ASSIST_METADATA & { duetProject?: string };
61
+ }
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
+
178
+ function buildProjectRequest(configuredProjectId: string | undefined): AntigravityProjectRequest {
179
+ if (!configuredProjectId) {
180
+ return { metadata: ANTIGRAVITY_LOAD_CODE_ASSIST_METADATA };
181
+ }
182
+ return {
183
+ cloudaicompanionProject: configuredProjectId,
184
+ metadata: {
185
+ ...ANTIGRAVITY_LOAD_CODE_ASSIST_METADATA,
186
+ duetProject: configuredProjectId,
187
+ },
188
+ };
189
+ }
190
+
51
191
  function readProjectId(value: string | { id?: string } | undefined): string | undefined {
52
192
  if (typeof value === "string" && value.length > 0) {
53
193
  return value;
@@ -72,7 +212,7 @@ function getDefaultTierId(allowedTiers?: Array<{ id?: string; isDefault?: boolea
72
212
  async function onboardProjectWithRetries(
73
213
  endpoint: string,
74
214
  headers: Record<string, string>,
75
- onboardBody: { tierId: string; metadata: typeof ANTIGRAVITY_LOAD_CODE_ASSIST_METADATA },
215
+ onboardBody: { tierId: string } & AntigravityProjectRequest,
76
216
  onProgress?: (message: string) => void,
77
217
  ): Promise<string> {
78
218
  for (let attempt = 1; attempt <= PROJECT_ONBOARD_MAX_ATTEMPTS; attempt += 1) {
@@ -108,7 +248,12 @@ async function onboardProjectWithRetries(
108
248
  );
109
249
  }
110
250
 
111
- async function discoverProject(accessToken: string, onProgress?: (message: string) => void): Promise<string> {
251
+ async function discoverProject(
252
+ accessToken: string,
253
+ configuredProjectId: string | undefined,
254
+ onProgress?: (message: string) => void,
255
+ ): Promise<string> {
256
+ const projectRequest = buildProjectRequest(configuredProjectId);
112
257
  const headers = {
113
258
  Authorization: `Bearer ${accessToken}`,
114
259
  "Content-Type": "application/json",
@@ -121,9 +266,7 @@ async function discoverProject(accessToken: string, onProgress?: (message: strin
121
266
  const loadResponse = await fetch(`${endpoint}/v1internal:loadCodeAssist`, {
122
267
  method: "POST",
123
268
  headers,
124
- body: JSON.stringify({
125
- metadata: ANTIGRAVITY_LOAD_CODE_ASSIST_METADATA,
126
- }),
269
+ body: JSON.stringify(projectRequest),
127
270
  });
128
271
 
129
272
  if (!loadResponse.ok) {
@@ -134,17 +277,17 @@ async function discoverProject(accessToken: string, onProgress?: (message: strin
134
277
  const loadPayload = (await loadResponse.json()) as LoadCodeAssistPayload;
135
278
  const existingProject = readProjectId(loadPayload.cloudaicompanionProject);
136
279
  if (existingProject) {
137
- return existingProject;
280
+ return configuredProjectId ?? existingProject;
138
281
  }
139
282
 
140
283
  const tierId = getDefaultTierId(loadPayload.allowedTiers);
141
284
  onProgress?.("Provisioning project...");
142
285
  const onboardBody = {
143
286
  tierId,
144
- metadata: ANTIGRAVITY_LOAD_CODE_ASSIST_METADATA,
287
+ ...projectRequest,
145
288
  };
146
289
  const provisionedProject = await onboardProjectWithRetries(endpoint, headers, onboardBody, onProgress);
147
- return provisionedProject;
290
+ return configuredProjectId ?? provisionedProject;
148
291
  } catch (error) {
149
292
  throw new Error(
150
293
  `Could not discover or provision an Antigravity project. ${error instanceof Error ? error.message : String(error)}`,
@@ -169,8 +312,11 @@ async function getUserEmail(accessToken: string): Promise<string | undefined> {
169
312
  }
170
313
 
171
314
  class AntigravityOAuthFlow extends OAuthCallbackFlow {
172
- constructor(ctrl: OAuthController) {
315
+ #configuredProjectId: string | undefined;
316
+
317
+ constructor(ctrl: OAuthController, configuredProjectId: string | undefined) {
173
318
  super(ctrl, CALLBACK_PORT, CALLBACK_PATH);
319
+ this.#configuredProjectId = configuredProjectId;
174
320
  }
175
321
 
176
322
  async generateAuthUrl(state: string, redirectUri: string): Promise<{ url: string; instructions?: string }> {
@@ -220,7 +366,7 @@ class AntigravityOAuthFlow extends OAuthCallbackFlow {
220
366
 
221
367
  this.ctrl.onProgress?.("Getting user info...");
222
368
  const email = await getUserEmail(tokenData.access_token);
223
- const projectId = await discoverProject(tokenData.access_token, this.ctrl.onProgress);
369
+ const projectId = await discoverProject(tokenData.access_token, this.#configuredProjectId, this.ctrl.onProgress);
224
370
 
225
371
  return {
226
372
  refresh: tokenData.refresh_token,
@@ -235,8 +381,12 @@ class AntigravityOAuthFlow extends OAuthCallbackFlow {
235
381
  /**
236
382
  * Login with Antigravity OAuth
237
383
  */
238
- export async function loginAntigravity(ctrl: OAuthController): Promise<OAuthCredentials> {
239
- 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);
240
390
  return flow.login();
241
391
  }
242
392