geminisdk 0.1.1
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 +129 -0
- package/dist/chunk-Y4MC6YDW.mjs +135 -0
- package/dist/index.d.mts +511 -0
- package/dist/index.d.ts +511 -0
- package/dist/index.js +1696 -0
- package/dist/index.mjs +1480 -0
- package/dist/types-ADTG4FSI.mjs +46 -0
- package/package.json +60 -0
- package/src/auth.ts +293 -0
- package/src/backend.ts +615 -0
- package/src/client.ts +230 -0
- package/src/exceptions.ts +289 -0
- package/src/index.ts +148 -0
- package/src/session.ts +380 -0
- package/src/tools.ts +127 -0
- package/src/types.ts +352 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import {
|
|
2
|
+
EventType,
|
|
3
|
+
GEMINI_CLI_MODELS,
|
|
4
|
+
GEMINI_CODE_ASSIST_API_VERSION,
|
|
5
|
+
GEMINI_CODE_ASSIST_ENDPOINT,
|
|
6
|
+
GEMINI_CREDENTIAL_FILENAME,
|
|
7
|
+
GEMINI_DIR,
|
|
8
|
+
GEMINI_ENV_FILENAME,
|
|
9
|
+
GEMINI_OAUTH_AUTH_ENDPOINT,
|
|
10
|
+
GEMINI_OAUTH_BASE_URL,
|
|
11
|
+
GEMINI_OAUTH_CLIENT_ID,
|
|
12
|
+
GEMINI_OAUTH_CLIENT_SECRET,
|
|
13
|
+
GEMINI_OAUTH_REDIRECT_URI,
|
|
14
|
+
GEMINI_OAUTH_SCOPES,
|
|
15
|
+
GEMINI_OAUTH_TOKEN_ENDPOINT,
|
|
16
|
+
HTTP_FORBIDDEN,
|
|
17
|
+
HTTP_OK,
|
|
18
|
+
HTTP_UNAUTHORIZED,
|
|
19
|
+
Role,
|
|
20
|
+
TOKEN_REFRESH_BUFFER_MS,
|
|
21
|
+
getGeminiCliCredentialPath,
|
|
22
|
+
getGeminiCliEnvPath
|
|
23
|
+
} from "./chunk-Y4MC6YDW.mjs";
|
|
24
|
+
export {
|
|
25
|
+
EventType,
|
|
26
|
+
GEMINI_CLI_MODELS,
|
|
27
|
+
GEMINI_CODE_ASSIST_API_VERSION,
|
|
28
|
+
GEMINI_CODE_ASSIST_ENDPOINT,
|
|
29
|
+
GEMINI_CREDENTIAL_FILENAME,
|
|
30
|
+
GEMINI_DIR,
|
|
31
|
+
GEMINI_ENV_FILENAME,
|
|
32
|
+
GEMINI_OAUTH_AUTH_ENDPOINT,
|
|
33
|
+
GEMINI_OAUTH_BASE_URL,
|
|
34
|
+
GEMINI_OAUTH_CLIENT_ID,
|
|
35
|
+
GEMINI_OAUTH_CLIENT_SECRET,
|
|
36
|
+
GEMINI_OAUTH_REDIRECT_URI,
|
|
37
|
+
GEMINI_OAUTH_SCOPES,
|
|
38
|
+
GEMINI_OAUTH_TOKEN_ENDPOINT,
|
|
39
|
+
HTTP_FORBIDDEN,
|
|
40
|
+
HTTP_OK,
|
|
41
|
+
HTTP_UNAUTHORIZED,
|
|
42
|
+
Role,
|
|
43
|
+
TOKEN_REFRESH_BUFFER_MS,
|
|
44
|
+
getGeminiCliCredentialPath,
|
|
45
|
+
getGeminiCliEnvPath
|
|
46
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "geminisdk",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "TypeScript SDK for Google Gemini Code Assist API - similar to GitHub Copilot SDK",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"module": "dist/index.mjs",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.mjs",
|
|
12
|
+
"require": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"src"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsup src/index.ts --format cjs,esm --dts",
|
|
21
|
+
"dev": "tsup src/index.ts --format cjs,esm --dts --watch",
|
|
22
|
+
"lint": "eslint src --ext .ts",
|
|
23
|
+
"test": "vitest",
|
|
24
|
+
"typecheck": "tsc --noEmit",
|
|
25
|
+
"prepublishOnly": "npm run build"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"gemini",
|
|
29
|
+
"google",
|
|
30
|
+
"ai",
|
|
31
|
+
"sdk",
|
|
32
|
+
"code-assist",
|
|
33
|
+
"llm",
|
|
34
|
+
"typescript"
|
|
35
|
+
],
|
|
36
|
+
"author": "OEvortex",
|
|
37
|
+
"license": "MIT",
|
|
38
|
+
"repository": {
|
|
39
|
+
"type": "git",
|
|
40
|
+
"url": "https://github.com/OEvortex/geminicli-sdk"
|
|
41
|
+
},
|
|
42
|
+
"bugs": {
|
|
43
|
+
"url": "https://github.com/OEvortex/geminicli-sdk/issues"
|
|
44
|
+
},
|
|
45
|
+
"homepage": "https://github.com/OEvortex/geminicli-sdk#readme",
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"uuid": "^9.0.0"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@types/node": "^20.10.0",
|
|
51
|
+
"@types/uuid": "^9.0.0",
|
|
52
|
+
"eslint": "^8.55.0",
|
|
53
|
+
"tsup": "^8.0.1",
|
|
54
|
+
"typescript": "^5.3.3",
|
|
55
|
+
"vitest": "^1.0.4"
|
|
56
|
+
},
|
|
57
|
+
"engines": {
|
|
58
|
+
"node": ">=18.0.0"
|
|
59
|
+
}
|
|
60
|
+
}
|
package/src/auth.ts
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth authentication for Gemini CLI / Code Assist API.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as fs from 'fs';
|
|
6
|
+
import * as path from 'path';
|
|
7
|
+
import {
|
|
8
|
+
AuthenticationError,
|
|
9
|
+
CredentialsNotFoundError,
|
|
10
|
+
TokenRefreshError,
|
|
11
|
+
} from './exceptions.js';
|
|
12
|
+
import {
|
|
13
|
+
GeminiOAuthCredentials,
|
|
14
|
+
GEMINI_CODE_ASSIST_API_VERSION,
|
|
15
|
+
GEMINI_CODE_ASSIST_ENDPOINT,
|
|
16
|
+
GEMINI_OAUTH_AUTH_ENDPOINT,
|
|
17
|
+
GEMINI_OAUTH_CLIENT_ID,
|
|
18
|
+
GEMINI_OAUTH_CLIENT_SECRET,
|
|
19
|
+
GEMINI_OAUTH_REDIRECT_URI,
|
|
20
|
+
GEMINI_OAUTH_SCOPES,
|
|
21
|
+
GEMINI_OAUTH_TOKEN_ENDPOINT,
|
|
22
|
+
HTTP_OK,
|
|
23
|
+
TOKEN_REFRESH_BUFFER_MS,
|
|
24
|
+
getGeminiCliCredentialPath,
|
|
25
|
+
getGeminiCliEnvPath,
|
|
26
|
+
} from './types.js';
|
|
27
|
+
|
|
28
|
+
export class GeminiOAuthManager {
|
|
29
|
+
private oauthPath?: string;
|
|
30
|
+
private clientId: string;
|
|
31
|
+
private clientSecret: string;
|
|
32
|
+
private credentials: GeminiOAuthCredentials | null = null;
|
|
33
|
+
private refreshLock: Promise<GeminiOAuthCredentials> | null = null;
|
|
34
|
+
private projectId: string | null = null;
|
|
35
|
+
|
|
36
|
+
constructor(
|
|
37
|
+
oauthPath?: string,
|
|
38
|
+
clientId?: string,
|
|
39
|
+
clientSecret?: string
|
|
40
|
+
) {
|
|
41
|
+
this.oauthPath = oauthPath;
|
|
42
|
+
this.clientId = clientId ?? GEMINI_OAUTH_CLIENT_ID;
|
|
43
|
+
this.clientSecret = clientSecret ?? GEMINI_OAUTH_CLIENT_SECRET;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private getCredentialPath(): string {
|
|
47
|
+
return getGeminiCliCredentialPath(this.oauthPath);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
private loadCachedCredentials(): GeminiOAuthCredentials {
|
|
51
|
+
const keyFile = this.getCredentialPath();
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const data = JSON.parse(fs.readFileSync(keyFile, 'utf-8')) as Record<string, unknown>;
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
accessToken: data['access_token'] as string,
|
|
58
|
+
refreshToken: data['refresh_token'] as string,
|
|
59
|
+
tokenType: (data['token_type'] as string) ?? 'Bearer',
|
|
60
|
+
expiryDate: (data['expiry_date'] as number) ?? 0,
|
|
61
|
+
};
|
|
62
|
+
} catch (error) {
|
|
63
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
64
|
+
throw new CredentialsNotFoundError(keyFile);
|
|
65
|
+
}
|
|
66
|
+
throw new AuthenticationError(
|
|
67
|
+
`Invalid Gemini OAuth credentials file at ${keyFile}: ${error}`
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private saveCredentials(credentials: GeminiOAuthCredentials): void {
|
|
73
|
+
const keyFile = this.getCredentialPath();
|
|
74
|
+
const dir = path.dirname(keyFile);
|
|
75
|
+
|
|
76
|
+
if (!fs.existsSync(dir)) {
|
|
77
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const data = {
|
|
81
|
+
access_token: credentials.accessToken,
|
|
82
|
+
refresh_token: credentials.refreshToken,
|
|
83
|
+
token_type: credentials.tokenType,
|
|
84
|
+
expiry_date: credentials.expiryDate,
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
fs.writeFileSync(keyFile, JSON.stringify(data, null, 2), 'utf-8');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private async refreshAccessToken(
|
|
91
|
+
credentials: GeminiOAuthCredentials
|
|
92
|
+
): Promise<GeminiOAuthCredentials> {
|
|
93
|
+
// Check if refresh is already in progress
|
|
94
|
+
if (this.refreshLock) {
|
|
95
|
+
return this.refreshLock;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
this.refreshLock = this.doRefresh(credentials);
|
|
99
|
+
try {
|
|
100
|
+
return await this.refreshLock;
|
|
101
|
+
} finally {
|
|
102
|
+
this.refreshLock = null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private async doRefresh(
|
|
107
|
+
credentials: GeminiOAuthCredentials
|
|
108
|
+
): Promise<GeminiOAuthCredentials> {
|
|
109
|
+
if (!credentials.refreshToken) {
|
|
110
|
+
throw new TokenRefreshError('No refresh token available in credentials.');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const bodyData = new URLSearchParams({
|
|
114
|
+
grant_type: 'refresh_token',
|
|
115
|
+
refresh_token: credentials.refreshToken,
|
|
116
|
+
client_id: this.clientId,
|
|
117
|
+
client_secret: this.clientSecret,
|
|
118
|
+
scope: GEMINI_OAUTH_SCOPES.join(' '),
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const response = await fetch(GEMINI_OAUTH_TOKEN_ENDPOINT, {
|
|
122
|
+
method: 'POST',
|
|
123
|
+
headers: {
|
|
124
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
125
|
+
Accept: 'application/json',
|
|
126
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
|
127
|
+
},
|
|
128
|
+
body: bodyData.toString(),
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
if (response.status !== HTTP_OK) {
|
|
132
|
+
throw new TokenRefreshError(
|
|
133
|
+
`Token refresh failed: ${response.status} ${response.statusText}`,
|
|
134
|
+
response.status,
|
|
135
|
+
await response.text()
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const tokenData = (await response.json()) as Record<string, unknown>;
|
|
140
|
+
|
|
141
|
+
if (tokenData['error']) {
|
|
142
|
+
throw new TokenRefreshError(
|
|
143
|
+
`Token refresh failed: ${tokenData['error']} - ${tokenData['error_description'] ?? 'Unknown error'}`
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const newCredentials: GeminiOAuthCredentials = {
|
|
148
|
+
accessToken: tokenData['access_token'] as string,
|
|
149
|
+
tokenType: (tokenData['token_type'] as string) ?? 'Bearer',
|
|
150
|
+
refreshToken: (tokenData['refresh_token'] as string) ?? credentials.refreshToken,
|
|
151
|
+
expiryDate:
|
|
152
|
+
Date.now() + ((tokenData['expires_in'] as number) ?? 3600) * 1000,
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
this.saveCredentials(newCredentials);
|
|
156
|
+
this.credentials = newCredentials;
|
|
157
|
+
|
|
158
|
+
return newCredentials;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
private isTokenValid(credentials: GeminiOAuthCredentials): boolean {
|
|
162
|
+
if (!credentials.expiryDate) return false;
|
|
163
|
+
return Date.now() < credentials.expiryDate - TOKEN_REFRESH_BUFFER_MS;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
public invalidateCredentials(): void {
|
|
167
|
+
this.credentials = null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
public async ensureAuthenticated(forceRefresh = false): Promise<string> {
|
|
171
|
+
if (this.credentials === null) {
|
|
172
|
+
this.credentials = this.loadCachedCredentials();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (forceRefresh || !this.isTokenValid(this.credentials)) {
|
|
176
|
+
this.credentials = await this.refreshAccessToken(this.credentials);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return this.credentials.accessToken;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
public async getCredentials(): Promise<GeminiOAuthCredentials> {
|
|
183
|
+
await this.ensureAuthenticated();
|
|
184
|
+
return this.credentials!;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
public getApiEndpoint(): string {
|
|
188
|
+
return `${GEMINI_CODE_ASSIST_ENDPOINT}/${GEMINI_CODE_ASSIST_API_VERSION}`;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
public getProjectId(): string | null {
|
|
192
|
+
const envProjectId = process.env['GOOGLE_CLOUD_PROJECT'];
|
|
193
|
+
if (envProjectId) return envProjectId;
|
|
194
|
+
|
|
195
|
+
const envFile = getGeminiCliEnvPath(
|
|
196
|
+
this.oauthPath ? path.dirname(this.getCredentialPath()) : undefined
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
if (fs.existsSync(envFile)) {
|
|
200
|
+
try {
|
|
201
|
+
const content = fs.readFileSync(envFile, 'utf-8');
|
|
202
|
+
for (const line of content.split('\n')) {
|
|
203
|
+
const trimmed = line.trim();
|
|
204
|
+
if (trimmed.startsWith('GOOGLE_CLOUD_PROJECT=')) {
|
|
205
|
+
return trimmed.split('=')[1]?.trim().replace(/['"]/g, '') ?? null;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
} catch {
|
|
209
|
+
// Ignore
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return this.projectId;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
public setProjectId(projectId: string): void {
|
|
217
|
+
this.projectId = projectId;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
public generateAuthUrl(state: string, codeVerifier?: string): string {
|
|
221
|
+
const params = new URLSearchParams({
|
|
222
|
+
client_id: this.clientId,
|
|
223
|
+
redirect_uri: GEMINI_OAUTH_REDIRECT_URI,
|
|
224
|
+
response_type: 'code',
|
|
225
|
+
scope: GEMINI_OAUTH_SCOPES.join(' '),
|
|
226
|
+
access_type: 'offline',
|
|
227
|
+
state,
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
if (codeVerifier) {
|
|
231
|
+
const encoder = new TextEncoder();
|
|
232
|
+
const data = encoder.encode(codeVerifier);
|
|
233
|
+
// Create SHA-256 hash for PKCE
|
|
234
|
+
// In real implementation, you'd use crypto.subtle.digest
|
|
235
|
+
params.set('code_challenge_method', 'S256');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return `${GEMINI_OAUTH_AUTH_ENDPOINT}?${params.toString()}`;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
public async exchangeCode(
|
|
242
|
+
code: string,
|
|
243
|
+
codeVerifier?: string
|
|
244
|
+
): Promise<GeminiOAuthCredentials> {
|
|
245
|
+
const bodyData = new URLSearchParams({
|
|
246
|
+
grant_type: 'authorization_code',
|
|
247
|
+
code,
|
|
248
|
+
client_id: this.clientId,
|
|
249
|
+
client_secret: this.clientSecret,
|
|
250
|
+
redirect_uri: GEMINI_OAUTH_REDIRECT_URI,
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
if (codeVerifier) {
|
|
254
|
+
bodyData.set('code_verifier', codeVerifier);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const response = await fetch(GEMINI_OAUTH_TOKEN_ENDPOINT, {
|
|
258
|
+
method: 'POST',
|
|
259
|
+
headers: {
|
|
260
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
261
|
+
Accept: 'application/json',
|
|
262
|
+
},
|
|
263
|
+
body: bodyData.toString(),
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
if (response.status !== HTTP_OK) {
|
|
267
|
+
throw new AuthenticationError(
|
|
268
|
+
`Code exchange failed: ${response.status} - ${await response.text()}`
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const tokenData = (await response.json()) as Record<string, unknown>;
|
|
273
|
+
|
|
274
|
+
if (tokenData['error']) {
|
|
275
|
+
throw new AuthenticationError(
|
|
276
|
+
`Code exchange failed: ${tokenData['error']} - ${tokenData['error_description'] ?? 'Unknown error'}`
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const credentials: GeminiOAuthCredentials = {
|
|
281
|
+
accessToken: tokenData['access_token'] as string,
|
|
282
|
+
refreshToken: tokenData['refresh_token'] as string,
|
|
283
|
+
tokenType: (tokenData['token_type'] as string) ?? 'Bearer',
|
|
284
|
+
expiryDate:
|
|
285
|
+
Date.now() + ((tokenData['expires_in'] as number) ?? 3600) * 1000,
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
this.saveCredentials(credentials);
|
|
289
|
+
this.credentials = credentials;
|
|
290
|
+
|
|
291
|
+
return credentials;
|
|
292
|
+
}
|
|
293
|
+
}
|