@link-assistant/agent 0.1.4 → 0.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/agent",
3
- "version": "0.1.4",
3
+ "version": "0.2.0",
4
4
  "description": "A minimal, public domain AI CLI agent compatible with OpenCode's JSON interface. Bun-only runtime.",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -1,4 +1,6 @@
1
1
  import crypto from 'crypto';
2
+ import * as http from 'node:http';
3
+ import * as net from 'node:net';
2
4
  import { Auth } from './index';
3
5
  import { Log } from '../util/log';
4
6
 
@@ -832,6 +834,279 @@ const OpenAIPlugin: AuthPlugin = {
832
834
  },
833
835
  };
834
836
 
837
+ /**
838
+ * Google OAuth Configuration
839
+ * Used for Google AI Pro/Ultra subscription authentication
840
+ *
841
+ * These credentials are from the official Gemini CLI (google-gemini/gemini-cli)
842
+ * and are public for installed applications as per Google OAuth documentation:
843
+ * https://developers.google.com/identity/protocols/oauth2#installed
844
+ */
845
+ const GOOGLE_OAUTH_CLIENT_ID =
846
+ '681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com';
847
+ const GOOGLE_OAUTH_CLIENT_SECRET = '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';
848
+ const GOOGLE_OAUTH_SCOPES = [
849
+ 'https://www.googleapis.com/auth/cloud-platform',
850
+ 'https://www.googleapis.com/auth/userinfo.email',
851
+ 'https://www.googleapis.com/auth/userinfo.profile',
852
+ ];
853
+
854
+ // Google OAuth endpoints
855
+ const GOOGLE_AUTH_URL = 'https://accounts.google.com/o/oauth2/v2/auth';
856
+ const GOOGLE_TOKEN_URL = 'https://oauth2.googleapis.com/token';
857
+ const GOOGLE_USERINFO_URL = 'https://www.googleapis.com/oauth2/v2/userinfo';
858
+
859
+ /**
860
+ * Google OAuth Plugin
861
+ * Supports:
862
+ * - Google AI Pro/Ultra OAuth login
863
+ * - Manual API key entry
864
+ *
865
+ * Note: This plugin uses OAuth 2.0 with PKCE for Google AI subscription authentication.
866
+ * After authenticating, you can use Gemini models with subscription benefits.
867
+ */
868
+ const GooglePlugin: AuthPlugin = {
869
+ provider: 'google',
870
+ methods: [
871
+ {
872
+ label: 'Google AI Pro/Ultra (OAuth)',
873
+ type: 'oauth',
874
+ async authorize() {
875
+ const pkce = await generatePKCE();
876
+ const state = generateRandomString(16);
877
+
878
+ // Start local server to handle OAuth redirect
879
+ const server = http.createServer();
880
+ let serverPort = 0;
881
+ let authCode: string | null = null;
882
+ let authState: string | null = null;
883
+
884
+ const authPromise = new Promise<{ code: string; state: string }>(
885
+ (resolve, reject) => {
886
+ server.on('request', (req, res) => {
887
+ const url = new URL(req.url!, `http://localhost:${serverPort}`);
888
+ const code = url.searchParams.get('code');
889
+ const receivedState = url.searchParams.get('state');
890
+ const error = url.searchParams.get('error');
891
+
892
+ if (error) {
893
+ res.writeHead(400, { 'Content-Type': 'text/html' });
894
+ res.end(`
895
+ <html>
896
+ <body>
897
+ <h1>Authentication Failed</h1>
898
+ <p>Error: ${error}</p>
899
+ <p>You can close this window.</p>
900
+ </body>
901
+ </html>
902
+ `);
903
+ server.close();
904
+ reject(new Error(`OAuth error: ${error}`));
905
+ return;
906
+ }
907
+
908
+ if (code && receivedState) {
909
+ if (receivedState !== state) {
910
+ res.writeHead(400, { 'Content-Type': 'text/html' });
911
+ res.end('Invalid state parameter');
912
+ server.close();
913
+ reject(new Error('State mismatch - possible CSRF attack'));
914
+ return;
915
+ }
916
+
917
+ res.writeHead(200, { 'Content-Type': 'text/html' });
918
+ res.end(`
919
+ <html>
920
+ <body>
921
+ <h1>Authentication Successful!</h1>
922
+ <p>You can close this window and return to the terminal.</p>
923
+ <script>window.close();</script>
924
+ </body>
925
+ </html>
926
+ `);
927
+ server.close();
928
+ resolve({ code, state: receivedState });
929
+ return;
930
+ }
931
+
932
+ res.writeHead(400, { 'Content-Type': 'text/html' });
933
+ res.end('Missing code or state parameter');
934
+ });
935
+
936
+ server.listen(0, () => {
937
+ const address = server.address() as net.AddressInfo;
938
+ serverPort = address.port;
939
+ });
940
+
941
+ server.on('error', reject);
942
+
943
+ // Timeout after 5 minutes
944
+ setTimeout(
945
+ () => {
946
+ server.close();
947
+ reject(new Error('OAuth timeout'));
948
+ },
949
+ 5 * 60 * 1000
950
+ );
951
+ }
952
+ );
953
+
954
+ // Build authorization URL with local redirect URI
955
+ const redirectUri = `http://localhost:${serverPort}/oauth/callback`;
956
+ const url = new URL(GOOGLE_AUTH_URL);
957
+ url.searchParams.set('client_id', GOOGLE_OAUTH_CLIENT_ID);
958
+ url.searchParams.set('redirect_uri', redirectUri);
959
+ url.searchParams.set('response_type', 'code');
960
+ url.searchParams.set('scope', GOOGLE_OAUTH_SCOPES.join(' '));
961
+ url.searchParams.set('access_type', 'offline');
962
+ url.searchParams.set('code_challenge', pkce.challenge);
963
+ url.searchParams.set('code_challenge_method', 'S256');
964
+ url.searchParams.set('state', state);
965
+ url.searchParams.set('prompt', 'consent');
966
+
967
+ return {
968
+ url: url.toString(),
969
+ instructions:
970
+ 'Your browser will open for authentication. Complete the login and return to the terminal.',
971
+ method: 'auto' as const,
972
+ async callback(): Promise<AuthResult> {
973
+ try {
974
+ const { code } = await authPromise;
975
+
976
+ // Exchange authorization code for tokens
977
+ const tokenResult = await fetch(GOOGLE_TOKEN_URL, {
978
+ method: 'POST',
979
+ headers: {
980
+ 'Content-Type': 'application/x-www-form-urlencoded',
981
+ },
982
+ body: new URLSearchParams({
983
+ code: code,
984
+ client_id: GOOGLE_OAUTH_CLIENT_ID,
985
+ client_secret: GOOGLE_OAUTH_CLIENT_SECRET,
986
+ redirect_uri: redirectUri,
987
+ grant_type: 'authorization_code',
988
+ code_verifier: pkce.verifier,
989
+ }),
990
+ });
991
+
992
+ if (!tokenResult.ok) {
993
+ log.error('google oauth token exchange failed', {
994
+ status: tokenResult.status,
995
+ });
996
+ return { type: 'failed' };
997
+ }
998
+
999
+ const json = await tokenResult.json();
1000
+ if (
1001
+ !json.access_token ||
1002
+ !json.refresh_token ||
1003
+ typeof json.expires_in !== 'number'
1004
+ ) {
1005
+ log.error('google oauth token response missing fields');
1006
+ return { type: 'failed' };
1007
+ }
1008
+
1009
+ return {
1010
+ type: 'success',
1011
+ refresh: json.refresh_token,
1012
+ access: json.access_token,
1013
+ expires: Date.now() + json.expires_in * 1000,
1014
+ };
1015
+ } catch (error) {
1016
+ log.error('google oauth failed', { error });
1017
+ return { type: 'failed' };
1018
+ }
1019
+ },
1020
+ };
1021
+ },
1022
+ },
1023
+ {
1024
+ label: 'Manually enter API Key',
1025
+ type: 'api',
1026
+ },
1027
+ ],
1028
+ async loader(getAuth, provider) {
1029
+ const auth = await getAuth();
1030
+ if (!auth || auth.type !== 'oauth') return {};
1031
+
1032
+ // Zero out cost for subscription users
1033
+ if (provider?.models) {
1034
+ for (const model of Object.values(provider.models)) {
1035
+ (model as any).cost = {
1036
+ input: 0,
1037
+ output: 0,
1038
+ cache: {
1039
+ read: 0,
1040
+ write: 0,
1041
+ },
1042
+ };
1043
+ }
1044
+ }
1045
+
1046
+ return {
1047
+ apiKey: 'oauth-token-used-via-custom-fetch',
1048
+ async fetch(input: RequestInfo | URL, init?: RequestInit) {
1049
+ let currentAuth = await getAuth();
1050
+ if (!currentAuth || currentAuth.type !== 'oauth')
1051
+ return fetch(input, init);
1052
+
1053
+ // Refresh token if expired (with 5 minute buffer)
1054
+ const FIVE_MIN_MS = 5 * 60 * 1000;
1055
+ if (
1056
+ !currentAuth.access ||
1057
+ currentAuth.expires < Date.now() + FIVE_MIN_MS
1058
+ ) {
1059
+ log.info('refreshing google oauth token');
1060
+ const response = await fetch(GOOGLE_TOKEN_URL, {
1061
+ method: 'POST',
1062
+ headers: {
1063
+ 'Content-Type': 'application/x-www-form-urlencoded',
1064
+ },
1065
+ body: new URLSearchParams({
1066
+ client_id: GOOGLE_OAUTH_CLIENT_ID,
1067
+ client_secret: GOOGLE_OAUTH_CLIENT_SECRET,
1068
+ refresh_token: currentAuth.refresh,
1069
+ grant_type: 'refresh_token',
1070
+ }),
1071
+ });
1072
+
1073
+ if (!response.ok) {
1074
+ throw new Error(`Token refresh failed: ${response.status}`);
1075
+ }
1076
+
1077
+ const json = await response.json();
1078
+ await Auth.set('google', {
1079
+ type: 'oauth',
1080
+ // Google doesn't return a new refresh token on refresh
1081
+ refresh: currentAuth.refresh,
1082
+ access: json.access_token,
1083
+ expires: Date.now() + json.expires_in * 1000,
1084
+ });
1085
+ currentAuth = {
1086
+ type: 'oauth',
1087
+ refresh: currentAuth.refresh,
1088
+ access: json.access_token,
1089
+ expires: Date.now() + json.expires_in * 1000,
1090
+ };
1091
+ }
1092
+
1093
+ // Google API uses Bearer token authentication
1094
+ const headers: Record<string, string> = {
1095
+ ...(init?.headers as Record<string, string>),
1096
+ Authorization: `Bearer ${currentAuth.access}`,
1097
+ };
1098
+ // Remove any API key header if present since we're using OAuth
1099
+ delete headers['x-goog-api-key'];
1100
+
1101
+ return fetch(input, {
1102
+ ...init,
1103
+ headers,
1104
+ });
1105
+ },
1106
+ };
1107
+ },
1108
+ };
1109
+
835
1110
  /**
836
1111
  * Registry of all auth plugins
837
1112
  */
@@ -839,6 +1114,7 @@ const plugins: Record<string, AuthPlugin> = {
839
1114
  anthropic: AnthropicPlugin,
840
1115
  'github-copilot': GitHubCopilotPlugin,
841
1116
  openai: OpenAIPlugin,
1117
+ google: GooglePlugin,
842
1118
  };
843
1119
 
844
1120
  /**
@@ -319,6 +319,33 @@ export namespace Provider {
319
319
  options: {},
320
320
  };
321
321
  },
322
+ /**
323
+ * Google OAuth provider for Gemini subscription users
324
+ * Uses OAuth credentials from agent auth login (Google AI Pro/Ultra)
325
+ *
326
+ * To authenticate, run: agent auth google
327
+ */
328
+ google: async (input) => {
329
+ const auth = await Auth.get('google');
330
+ if (auth?.type === 'oauth') {
331
+ log.info('using google oauth credentials');
332
+ const loaderFn = await AuthPlugins.getLoader('google');
333
+ if (loaderFn) {
334
+ const result = await loaderFn(() => Auth.get('google'), input);
335
+ if (result.fetch) {
336
+ return {
337
+ autoload: true,
338
+ options: {
339
+ apiKey: result.apiKey || '',
340
+ fetch: result.fetch,
341
+ },
342
+ };
343
+ }
344
+ }
345
+ }
346
+ // Default: API key auth (no OAuth credentials found)
347
+ return { autoload: false };
348
+ },
322
349
  /**
323
350
  * GitHub Copilot OAuth provider
324
351
  * Uses OAuth credentials from agent auth login