@kaitranntt/ccs 7.52.2 → 7.53.0-dev.2
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 +9 -1
- package/config/base-claude.settings.json +3 -3
- package/config/base-codex.settings.json +1 -1
- package/config/base-llamacpp.settings.json +10 -0
- package/dist/api/services/index.d.ts +3 -1
- package/dist/api/services/index.d.ts.map +1 -1
- package/dist/api/services/index.js +10 -1
- package/dist/api/services/index.js.map +1 -1
- package/dist/api/services/profile-lifecycle-service.d.ts +24 -0
- package/dist/api/services/profile-lifecycle-service.d.ts.map +1 -0
- package/dist/api/services/profile-lifecycle-service.js +364 -0
- package/dist/api/services/profile-lifecycle-service.js.map +1 -0
- package/dist/api/services/profile-lifecycle-validation.d.ts +11 -0
- package/dist/api/services/profile-lifecycle-validation.d.ts.map +1 -0
- package/dist/api/services/profile-lifecycle-validation.js +87 -0
- package/dist/api/services/profile-lifecycle-validation.js.map +1 -0
- package/dist/api/services/profile-reader.js +1 -1
- package/dist/api/services/profile-reader.js.map +1 -1
- package/dist/api/services/profile-types.d.ts +66 -0
- package/dist/api/services/profile-types.d.ts.map +1 -1
- package/dist/api/services/profile-writer.d.ts.map +1 -1
- package/dist/api/services/profile-writer.js +23 -8
- package/dist/api/services/profile-writer.js.map +1 -1
- package/dist/auth/auth-commands.d.ts.map +1 -1
- package/dist/auth/auth-commands.js +4 -0
- package/dist/auth/auth-commands.js.map +1 -1
- package/dist/auth/commands/create-command.d.ts.map +1 -1
- package/dist/auth/commands/create-command.js +20 -7
- package/dist/auth/commands/create-command.js.map +1 -1
- package/dist/auth/commands/show-command.d.ts.map +1 -1
- package/dist/auth/commands/show-command.js +2 -0
- package/dist/auth/commands/show-command.js.map +1 -1
- package/dist/auth/commands/types.d.ts +2 -0
- package/dist/auth/commands/types.d.ts.map +1 -1
- package/dist/auth/commands/types.js +2 -0
- package/dist/auth/commands/types.js.map +1 -1
- package/dist/auth/profile-continuity-inheritance.d.ts.map +1 -1
- package/dist/auth/profile-continuity-inheritance.js +3 -1
- package/dist/auth/profile-continuity-inheritance.js.map +1 -1
- package/dist/auth/profile-detector.d.ts.map +1 -1
- package/dist/auth/profile-detector.js +1 -0
- package/dist/auth/profile-detector.js.map +1 -1
- package/dist/auth/profile-registry.d.ts +1 -0
- package/dist/auth/profile-registry.d.ts.map +1 -1
- package/dist/auth/profile-registry.js +3 -0
- package/dist/auth/profile-registry.js.map +1 -1
- package/dist/ccs.js +32 -4
- package/dist/ccs.js.map +1 -1
- package/dist/cliproxy/auth/auth-types.d.ts +3 -0
- package/dist/cliproxy/auth/auth-types.d.ts.map +1 -1
- package/dist/cliproxy/auth/auth-types.js +18 -1
- package/dist/cliproxy/auth/auth-types.js.map +1 -1
- package/dist/cliproxy/auth/gemini-token-refresh.d.ts.map +1 -1
- package/dist/cliproxy/auth/gemini-token-refresh.js +42 -14
- package/dist/cliproxy/auth/gemini-token-refresh.js.map +1 -1
- package/dist/cliproxy/auth/oauth-handler.d.ts +10 -0
- package/dist/cliproxy/auth/oauth-handler.d.ts.map +1 -1
- package/dist/cliproxy/auth/oauth-handler.js +60 -15
- package/dist/cliproxy/auth/oauth-handler.js.map +1 -1
- package/dist/cliproxy/binary-manager.d.ts.map +1 -1
- package/dist/cliproxy/binary-manager.js +13 -14
- package/dist/cliproxy/binary-manager.js.map +1 -1
- package/dist/cliproxy/config/thinking-config.d.ts.map +1 -1
- package/dist/cliproxy/config/thinking-config.js +9 -0
- package/dist/cliproxy/config/thinking-config.js.map +1 -1
- package/dist/cliproxy/model-catalog.d.ts.map +1 -1
- package/dist/cliproxy/model-catalog.js +14 -1
- package/dist/cliproxy/model-catalog.js.map +1 -1
- package/dist/cliproxy/quota-fetcher-claude.d.ts.map +1 -1
- package/dist/cliproxy/quota-fetcher-claude.js +51 -8
- package/dist/cliproxy/quota-fetcher-claude.js.map +1 -1
- package/dist/cliproxy/quota-fetcher-codex.d.ts.map +1 -1
- package/dist/cliproxy/quota-fetcher-codex.js +199 -63
- package/dist/cliproxy/quota-fetcher-codex.js.map +1 -1
- package/dist/cliproxy/quota-fetcher.d.ts +12 -0
- package/dist/cliproxy/quota-fetcher.d.ts.map +1 -1
- package/dist/cliproxy/quota-fetcher.js +328 -260
- package/dist/cliproxy/quota-fetcher.js.map +1 -1
- package/dist/cliproxy/quota-manager.d.ts +1 -1
- package/dist/cliproxy/quota-manager.d.ts.map +1 -1
- package/dist/cliproxy/quota-manager.js +9 -7
- package/dist/cliproxy/quota-manager.js.map +1 -1
- package/dist/cliproxy/quota-types.d.ts +18 -4
- package/dist/cliproxy/quota-types.d.ts.map +1 -1
- package/dist/cliproxy/thinking-validator.js +20 -0
- package/dist/cliproxy/thinking-validator.js.map +1 -1
- package/dist/cliproxy/tool-sanitization-proxy.d.ts.map +1 -1
- package/dist/cliproxy/tool-sanitization-proxy.js +86 -2
- package/dist/cliproxy/tool-sanitization-proxy.js.map +1 -1
- package/dist/commands/api-command.d.ts.map +1 -1
- package/dist/commands/api-command.js +299 -13
- package/dist/commands/api-command.js.map +1 -1
- package/dist/commands/copilot-command.d.ts.map +1 -1
- package/dist/commands/copilot-command.js +38 -0
- package/dist/commands/copilot-command.js.map +1 -1
- package/dist/commands/env-command.js +1 -1
- package/dist/commands/env-command.js.map +1 -1
- package/dist/commands/help-command.d.ts.map +1 -1
- package/dist/commands/help-command.js +7 -1
- package/dist/commands/help-command.js.map +1 -1
- package/dist/commands/sync-command.d.ts.map +1 -1
- package/dist/commands/sync-command.js +25 -0
- package/dist/commands/sync-command.js.map +1 -1
- package/dist/config/unified-config-types.d.ts +2 -0
- package/dist/config/unified-config-types.d.ts.map +1 -1
- package/dist/config/unified-config-types.js.map +1 -1
- package/dist/copilot/copilot-models.d.ts.map +1 -1
- package/dist/copilot/copilot-models.js +55 -13
- package/dist/copilot/copilot-models.js.map +1 -1
- package/dist/copilot/types.d.ts +10 -0
- package/dist/copilot/types.d.ts.map +1 -1
- package/dist/management/instance-manager.d.ts +9 -6
- package/dist/management/instance-manager.d.ts.map +1 -1
- package/dist/management/instance-manager.js +56 -34
- package/dist/management/instance-manager.js.map +1 -1
- package/dist/shared/provider-preset-catalog.d.ts +1 -1
- package/dist/shared/provider-preset-catalog.d.ts.map +1 -1
- package/dist/shared/provider-preset-catalog.js +30 -0
- package/dist/shared/provider-preset-catalog.js.map +1 -1
- package/dist/types/config.d.ts +2 -0
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/config.js.map +1 -1
- package/dist/ui/assets/accounts-PwWppkAw.js +1 -0
- package/dist/ui/assets/{alert-dialog-DPdKlUG9.js → alert-dialog-BKNsxsSg.js} +1 -1
- package/dist/ui/assets/api-C9f0vP-J.js +4 -0
- package/dist/ui/assets/auth-section-D0XQSyo0.js +1 -0
- package/dist/ui/assets/backups-section-Dxj8M3n7.js +1 -0
- package/dist/ui/assets/{checkbox-CcX8-GfD.js → checkbox-mjr1O0jA.js} +1 -1
- package/dist/ui/assets/cliproxy-BwRuqd53.js +3 -0
- package/dist/ui/assets/cliproxy-control-panel-VThtggLh.js +1 -0
- package/dist/ui/assets/{confirm-dialog-BZj0PYFs.js → confirm-dialog-i-Sfbmcm.js} +1 -1
- package/dist/ui/assets/copilot-Bp6z4OCx.js +3 -0
- package/dist/ui/assets/{cursor-rS1S0i_y.js → cursor-CJP1tf06.js} +1 -1
- package/dist/ui/assets/{droid-Dfc2QwbE.js → droid-R1Tbhk5J.js} +1 -1
- package/dist/ui/assets/{form-utils-Cn_Uld6y.js → form-utils-Bcoyqxpq.js} +1 -1
- package/dist/ui/assets/globalenv-section-TvE1onaY.js +1 -0
- package/dist/ui/assets/{health-B0WQPDXb.js → health-Cyjkkaf-.js} +1 -1
- package/dist/ui/assets/{icons-BYZM_9Gm.js → icons-D2eEmpHv.js} +1 -1
- package/dist/ui/assets/index-1DQih7xp.js +1 -0
- package/dist/ui/assets/index-BlXbW1dV.js +1 -0
- package/dist/ui/assets/index-BusjPRWX.css +1 -0
- package/dist/ui/assets/index-CcjPykyr.js +1 -0
- package/dist/ui/assets/{index-LHbr_5SB.js → index-DfbkdLOb.js} +1 -1
- package/dist/ui/assets/index-WhofYWgJ.js +47 -0
- package/dist/ui/assets/proxy-status-widget-O38AQ1k4.js +1 -0
- package/dist/ui/assets/{separator-DtcqgZIS.js → separator-B66kRzAj.js} +1 -1
- package/dist/ui/assets/shared-BG19Chhy.js +8 -0
- package/dist/ui/assets/{switch-BL5xZtnr.js → switch-IqCO_Age.js} +1 -1
- package/dist/ui/assets/{updates-B3HKUp7y.js → updates-_5C-OO-6.js} +1 -1
- package/dist/ui/index.html +4 -4
- package/dist/utils/api-key-validator.d.ts +2 -0
- package/dist/utils/api-key-validator.d.ts.map +1 -1
- package/dist/utils/api-key-validator.js +64 -1
- package/dist/utils/api-key-validator.js.map +1 -1
- package/dist/utils/delegation-validator.d.ts.map +1 -1
- package/dist/utils/delegation-validator.js +4 -3
- package/dist/utils/delegation-validator.js.map +1 -1
- package/dist/utils/error-codes.d.ts +1 -1
- package/dist/utils/error-codes.d.ts.map +1 -1
- package/dist/utils/error-codes.js +11 -6
- package/dist/utils/error-codes.js.map +1 -1
- package/dist/web-server/routes/account-routes.d.ts.map +1 -1
- package/dist/web-server/routes/account-routes.js +2 -1
- package/dist/web-server/routes/account-routes.js.map +1 -1
- package/dist/web-server/routes/cliproxy-stats-routes.d.ts +12 -0
- package/dist/web-server/routes/cliproxy-stats-routes.d.ts.map +1 -1
- package/dist/web-server/routes/cliproxy-stats-routes.js +22 -9
- package/dist/web-server/routes/cliproxy-stats-routes.js.map +1 -1
- package/dist/web-server/routes/profile-routes.d.ts.map +1 -1
- package/dist/web-server/routes/profile-routes.js +218 -18
- package/dist/web-server/routes/profile-routes.js.map +1 -1
- package/dist/web-server/routes/route-helpers.d.ts +1 -0
- package/dist/web-server/routes/route-helpers.d.ts.map +1 -1
- package/dist/web-server/routes/route-helpers.js +51 -13
- package/dist/web-server/routes/route-helpers.js.map +1 -1
- package/dist/web-server/routes/settings-routes.d.ts.map +1 -1
- package/dist/web-server/routes/settings-routes.js +3 -0
- package/dist/web-server/routes/settings-routes.js.map +1 -1
- package/dist/web-server/services/cliproxy-dashboard-install-service.d.ts +21 -0
- package/dist/web-server/services/cliproxy-dashboard-install-service.d.ts.map +1 -0
- package/dist/web-server/services/cliproxy-dashboard-install-service.js +51 -0
- package/dist/web-server/services/cliproxy-dashboard-install-service.js.map +1 -0
- package/lib/error-codes.ps1 +14 -4
- package/lib/error-codes.sh +10 -6
- package/lib/hooks/image-analyzer-transformer.cjs +51 -20
- package/package.json +2 -6
- package/dist/ui/assets/accounts-szAllF_0.js +0 -1
- package/dist/ui/assets/api-Bq7TnM0v.js +0 -1
- package/dist/ui/assets/auth-section-DYPWbNRj.js +0 -1
- package/dist/ui/assets/backups-section-gIWfCGmR.js +0 -1
- package/dist/ui/assets/cliproxy-SS8eRAX0.js +0 -3
- package/dist/ui/assets/cliproxy-control-panel-Bu0TtDft.js +0 -1
- package/dist/ui/assets/copilot-DnJj3frU.js +0 -3
- package/dist/ui/assets/globalenv-section-BrHb5lRq.js +0 -1
- package/dist/ui/assets/index-BFTIN2qO.js +0 -1
- package/dist/ui/assets/index-BVBXszJi.js +0 -1
- package/dist/ui/assets/index-DCQkhmoo.js +0 -47
- package/dist/ui/assets/index-DxKsP0Ke.js +0 -1
- package/dist/ui/assets/index-WBo504Wu.css +0 -1
- package/dist/ui/assets/proxy-status-widget-DPXgRGkB.js +0 -1
- package/dist/ui/assets/shared-DPJ_z23p.js +0 -8
- package/scripts/maintainability-baseline.js +0 -308
- package/scripts/maintainability-check.js +0 -163
|
@@ -35,14 +35,11 @@ const path = __importStar(require("node:path"));
|
|
|
35
35
|
const config_generator_1 = require("./config-generator");
|
|
36
36
|
const account_manager_1 = require("./account-manager");
|
|
37
37
|
const auth_utils_1 = require("./auth-utils");
|
|
38
|
+
const proxy_target_resolver_1 = require("./proxy-target-resolver");
|
|
38
39
|
/** Google Cloud Code API endpoints */
|
|
39
40
|
const ANTIGRAVITY_API_BASE = 'https://cloudcode-pa.googleapis.com';
|
|
40
41
|
const ANTIGRAVITY_API_VERSION = 'v1internal';
|
|
41
|
-
|
|
42
|
-
const GOOGLE_TOKEN_URL = 'https://oauth2.googleapis.com/token';
|
|
43
|
-
/** Antigravity OAuth credentials (from CLIProxyAPIPlus - public in open-source code) */
|
|
44
|
-
const ANTIGRAVITY_CLIENT_ID = '1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com';
|
|
45
|
-
const ANTIGRAVITY_CLIENT_SECRET = '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';
|
|
42
|
+
const MANAGEMENT_API_TIMEOUT_MS = 5000;
|
|
46
43
|
/** Headers for loadCodeAssist (matches CLIProxyAPI antigravity.go) */
|
|
47
44
|
const LOADCODEASSIST_HEADERS = {
|
|
48
45
|
'Content-Type': 'application/json',
|
|
@@ -55,61 +52,221 @@ const FETCHMODELS_HEADERS = {
|
|
|
55
52
|
'Content-Type': 'application/json',
|
|
56
53
|
'User-Agent': 'antigravity/1.104.0 darwin/arm64',
|
|
57
54
|
};
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
55
|
+
function safeParseJson(bodyText) {
|
|
56
|
+
try {
|
|
57
|
+
return JSON.parse(bodyText);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function normalizeErrorDetail(bodyText) {
|
|
64
|
+
const normalized = bodyText.trim();
|
|
65
|
+
if (!normalized) {
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
if (normalized.length <= 400) {
|
|
69
|
+
return normalized;
|
|
70
|
+
}
|
|
71
|
+
return `${normalized.slice(0, 397)}...`;
|
|
72
|
+
}
|
|
73
|
+
function buildAntigravityFailure(status, bodyText) {
|
|
74
|
+
const detail = normalizeErrorDetail(bodyText || '');
|
|
75
|
+
if (status === 401) {
|
|
76
|
+
return {
|
|
77
|
+
httpStatus: 401,
|
|
78
|
+
error: 'Token expired or invalid',
|
|
79
|
+
errorCode: 'reauth_required',
|
|
80
|
+
actionHint: 'Re-authenticate this account. If CLIProxy is running, retry after the proxy finishes refreshing the token.',
|
|
81
|
+
needsReauth: true,
|
|
82
|
+
errorDetail: detail,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
if (status === 403) {
|
|
86
|
+
return {
|
|
87
|
+
httpStatus: 403,
|
|
88
|
+
error: 'Access forbidden',
|
|
89
|
+
errorCode: 'quota_api_forbidden',
|
|
90
|
+
actionHint: 'This account does not have Gemini Code Assist quota access.',
|
|
91
|
+
isForbidden: true,
|
|
92
|
+
errorDetail: detail,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
if (status === 429) {
|
|
96
|
+
return {
|
|
97
|
+
httpStatus: 429,
|
|
98
|
+
error: 'Rate limited - try again later',
|
|
99
|
+
errorCode: 'rate_limited',
|
|
100
|
+
actionHint: 'Retry later. This looks temporary.',
|
|
101
|
+
retryable: true,
|
|
102
|
+
errorDetail: detail,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
if (status === 408) {
|
|
106
|
+
return {
|
|
107
|
+
httpStatus: 408,
|
|
108
|
+
error: 'Request timeout',
|
|
109
|
+
errorCode: 'network_timeout',
|
|
110
|
+
actionHint: 'Retry later. This looks temporary.',
|
|
111
|
+
retryable: true,
|
|
112
|
+
errorDetail: detail,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
if (typeof status === 'number' && status >= 500) {
|
|
116
|
+
return {
|
|
117
|
+
httpStatus: status,
|
|
118
|
+
error: `API error: ${status}`,
|
|
119
|
+
errorCode: 'provider_unavailable',
|
|
120
|
+
actionHint: 'Retry later. The provider appears unavailable.',
|
|
121
|
+
retryable: true,
|
|
122
|
+
errorDetail: detail,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
if (typeof status === 'number' && status >= 400) {
|
|
126
|
+
return {
|
|
127
|
+
httpStatus: status,
|
|
128
|
+
error: `API error: ${status}`,
|
|
129
|
+
errorCode: 'quota_request_failed',
|
|
130
|
+
errorDetail: detail,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
error: 'Quota request failed',
|
|
135
|
+
errorCode: 'quota_request_failed',
|
|
136
|
+
errorDetail: detail,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
async function readManagedResponse(response, viaManagement) {
|
|
140
|
+
const bodyText = await response.text();
|
|
141
|
+
return {
|
|
142
|
+
status: response.status,
|
|
143
|
+
bodyText,
|
|
144
|
+
json: safeParseJson(bodyText),
|
|
145
|
+
viaManagement,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
function isAntigravityAuthFileForAccount(file, accountId) {
|
|
149
|
+
const provider = (file.provider || file.type || '').trim().toLowerCase();
|
|
150
|
+
if (provider !== 'antigravity' && provider !== 'agy') {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
const normalizedAccount = accountId.trim().toLowerCase();
|
|
154
|
+
const normalizedEmail = file.email?.trim().toLowerCase();
|
|
155
|
+
if (normalizedEmail && normalizedEmail === normalizedAccount) {
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
const normalizedName = file.name?.trim().toLowerCase();
|
|
159
|
+
if (!normalizedName) {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
const sanitizedAccount = (0, auth_utils_1.sanitizeEmail)(accountId).toLowerCase();
|
|
163
|
+
return (normalizedName === `antigravity-${sanitizedAccount}.json` ||
|
|
164
|
+
normalizedName === `agy-${sanitizedAccount}.json`);
|
|
165
|
+
}
|
|
166
|
+
async function findManagedAntigravityAuthIndex(accountId) {
|
|
167
|
+
const target = (0, proxy_target_resolver_1.getProxyTarget)();
|
|
65
168
|
const controller = new AbortController();
|
|
66
|
-
const timeoutId = setTimeout(() => controller.abort(),
|
|
169
|
+
const timeoutId = setTimeout(() => controller.abort(), MANAGEMENT_API_TIMEOUT_MS);
|
|
67
170
|
try {
|
|
68
|
-
const response = await fetch(
|
|
171
|
+
const response = await fetch((0, proxy_target_resolver_1.buildProxyUrl)(target, '/v0/management/auth-files'), {
|
|
172
|
+
signal: controller.signal,
|
|
173
|
+
headers: (0, proxy_target_resolver_1.buildManagementHeaders)(target),
|
|
174
|
+
});
|
|
175
|
+
clearTimeout(timeoutId);
|
|
176
|
+
if (!response.ok) {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
const data = (await response.json());
|
|
180
|
+
const match = data.files?.find((file) => isAntigravityAuthFileForAccount(file, accountId));
|
|
181
|
+
return match?.auth_index ?? null;
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
clearTimeout(timeoutId);
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
async function performManagedAntigravityRequest(accountId, url, headers, body) {
|
|
189
|
+
const authIndex = await findManagedAntigravityAuthIndex(accountId);
|
|
190
|
+
if (authIndex === null || authIndex === undefined) {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
const target = (0, proxy_target_resolver_1.getProxyTarget)();
|
|
194
|
+
const controller = new AbortController();
|
|
195
|
+
const timeoutId = setTimeout(() => controller.abort(), MANAGEMENT_API_TIMEOUT_MS);
|
|
196
|
+
try {
|
|
197
|
+
const response = await fetch((0, proxy_target_resolver_1.buildProxyUrl)(target, '/v0/management/api-call'), {
|
|
198
|
+
method: 'POST',
|
|
199
|
+
signal: controller.signal,
|
|
200
|
+
headers: (0, proxy_target_resolver_1.buildManagementHeaders)(target, {
|
|
201
|
+
'Content-Type': 'application/json',
|
|
202
|
+
}),
|
|
203
|
+
body: JSON.stringify({
|
|
204
|
+
auth_index: authIndex,
|
|
205
|
+
method: 'POST',
|
|
206
|
+
url,
|
|
207
|
+
header: {
|
|
208
|
+
...headers,
|
|
209
|
+
Authorization: 'Bearer $TOKEN$',
|
|
210
|
+
},
|
|
211
|
+
data: body,
|
|
212
|
+
}),
|
|
213
|
+
});
|
|
214
|
+
clearTimeout(timeoutId);
|
|
215
|
+
if (!response.ok) {
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
const apiResponse = (await response.json());
|
|
219
|
+
const bodyText = typeof apiResponse.body === 'string' ? apiResponse.body : '';
|
|
220
|
+
return {
|
|
221
|
+
status: typeof apiResponse.status_code === 'number' ? apiResponse.status_code : 500,
|
|
222
|
+
bodyText,
|
|
223
|
+
json: safeParseJson(bodyText),
|
|
224
|
+
viaManagement: true,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
clearTimeout(timeoutId);
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
async function performAntigravityRequest(accountId, accessToken, url, headers, body) {
|
|
233
|
+
const controller = new AbortController();
|
|
234
|
+
const timeoutId = setTimeout(() => controller.abort(), MANAGEMENT_API_TIMEOUT_MS);
|
|
235
|
+
try {
|
|
236
|
+
const response = await fetch(url, {
|
|
69
237
|
method: 'POST',
|
|
70
238
|
signal: controller.signal,
|
|
71
239
|
headers: {
|
|
72
|
-
|
|
240
|
+
...headers,
|
|
241
|
+
Authorization: `Bearer ${accessToken}`,
|
|
73
242
|
},
|
|
74
|
-
body
|
|
75
|
-
grant_type: 'refresh_token',
|
|
76
|
-
refresh_token: refreshToken,
|
|
77
|
-
client_id: ANTIGRAVITY_CLIENT_ID,
|
|
78
|
-
client_secret: ANTIGRAVITY_CLIENT_SECRET,
|
|
79
|
-
}).toString(),
|
|
243
|
+
body,
|
|
80
244
|
});
|
|
81
245
|
clearTimeout(timeoutId);
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
if (!response.ok || data.error) {
|
|
86
|
-
const error = data.error_description || data.error || `OAuth error: ${response.status}`;
|
|
87
|
-
if (verbose)
|
|
88
|
-
console.error(`[!] Token refresh failed: ${error}`);
|
|
89
|
-
return {
|
|
90
|
-
accessToken: null,
|
|
91
|
-
error,
|
|
92
|
-
};
|
|
246
|
+
const directResult = await readManagedResponse(response, false);
|
|
247
|
+
if (directResult.status !== 401) {
|
|
248
|
+
return directResult;
|
|
93
249
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
console.error('[!] Token refresh failed: No access_token in response');
|
|
97
|
-
return { accessToken: null, error: 'No access_token in response' };
|
|
98
|
-
}
|
|
99
|
-
if (verbose)
|
|
100
|
-
console.error('[i] Token refresh: success');
|
|
101
|
-
return { accessToken: data.access_token };
|
|
250
|
+
const managedResult = await performManagedAntigravityRequest(accountId, url, headers, body);
|
|
251
|
+
return managedResult ?? directResult;
|
|
102
252
|
}
|
|
103
253
|
catch (err) {
|
|
104
254
|
clearTimeout(timeoutId);
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
:
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
255
|
+
if (err instanceof Error && err.name === 'AbortError') {
|
|
256
|
+
return {
|
|
257
|
+
status: 408,
|
|
258
|
+
bodyText: '',
|
|
259
|
+
json: null,
|
|
260
|
+
viaManagement: false,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
const message = err instanceof Error ? err.message : 'Unknown error';
|
|
264
|
+
return {
|
|
265
|
+
status: 503,
|
|
266
|
+
bodyText: message,
|
|
267
|
+
json: null,
|
|
268
|
+
viaManagement: false,
|
|
269
|
+
};
|
|
113
270
|
}
|
|
114
271
|
}
|
|
115
272
|
/**
|
|
@@ -198,173 +355,111 @@ function mapTierString(tierStr) {
|
|
|
198
355
|
* Get project ID and tier via loadCodeAssist endpoint
|
|
199
356
|
* Uses paidTier.id for accurate tier detection (g1-ultra-tier, g1-pro-tier)
|
|
200
357
|
*/
|
|
201
|
-
async function getProjectId(accessToken) {
|
|
358
|
+
async function getProjectId(accountId, accessToken) {
|
|
202
359
|
const url = `${ANTIGRAVITY_API_BASE}/${ANTIGRAVITY_API_VERSION}:loadCodeAssist`;
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
platform: 'PLATFORM_UNSPECIFIED',
|
|
217
|
-
pluginType: 'GEMINI',
|
|
218
|
-
},
|
|
219
|
-
}),
|
|
220
|
-
});
|
|
221
|
-
clearTimeout(timeoutId);
|
|
222
|
-
if (!response.ok) {
|
|
223
|
-
// Return specific error based on status
|
|
224
|
-
if (response.status === 401) {
|
|
225
|
-
return { projectId: null, error: 'Token expired or invalid' };
|
|
226
|
-
}
|
|
227
|
-
if (response.status === 403) {
|
|
228
|
-
return { projectId: null, error: 'Access forbidden' };
|
|
229
|
-
}
|
|
230
|
-
return { projectId: null, error: `API error: ${response.status}` };
|
|
231
|
-
}
|
|
232
|
-
const data = (await response.json());
|
|
233
|
-
// Extract project ID from response
|
|
234
|
-
let projectId;
|
|
235
|
-
if (typeof data.cloudaicompanionProject === 'string') {
|
|
236
|
-
projectId = data.cloudaicompanionProject;
|
|
237
|
-
}
|
|
238
|
-
else if (typeof data.cloudaicompanionProject === 'object') {
|
|
239
|
-
projectId = data.cloudaicompanionProject?.id;
|
|
240
|
-
}
|
|
241
|
-
if (!projectId?.trim()) {
|
|
242
|
-
// Account authenticated but not provisioned - user needs to sign in via Antigravity app
|
|
243
|
-
return {
|
|
244
|
-
projectId: null,
|
|
245
|
-
error: 'Sign in to Antigravity app to activate quota.',
|
|
246
|
-
isUnprovisioned: true,
|
|
247
|
-
};
|
|
248
|
-
}
|
|
249
|
-
// Extract tier - paidTier reflects actual subscription status, takes priority
|
|
250
|
-
// API returns: paidTier.id = "g1-ultra-tier" or "g1-pro-tier"
|
|
251
|
-
// allowedTiers/currentTier often return "standard-tier" which is not useful
|
|
252
|
-
const tierStr = data.paidTier?.id || data.currentTier?.id;
|
|
253
|
-
const tier = mapTierString(tierStr);
|
|
254
|
-
return { projectId: projectId.trim(), tier };
|
|
360
|
+
const body = JSON.stringify({
|
|
361
|
+
metadata: {
|
|
362
|
+
ideType: 'IDE_UNSPECIFIED',
|
|
363
|
+
platform: 'PLATFORM_UNSPECIFIED',
|
|
364
|
+
pluginType: 'GEMINI',
|
|
365
|
+
},
|
|
366
|
+
});
|
|
367
|
+
const response = await performAntigravityRequest(accountId, accessToken, url, LOADCODEASSIST_HEADERS, body);
|
|
368
|
+
if (response.status < 200 || response.status >= 300) {
|
|
369
|
+
return {
|
|
370
|
+
projectId: null,
|
|
371
|
+
...buildAntigravityFailure(response.status, response.bodyText),
|
|
372
|
+
};
|
|
255
373
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
374
|
+
const data = response.json;
|
|
375
|
+
if (!data) {
|
|
376
|
+
return {
|
|
377
|
+
projectId: null,
|
|
378
|
+
error: 'Invalid quota response from provider',
|
|
379
|
+
errorCode: 'provider_unavailable',
|
|
380
|
+
retryable: true,
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
// Extract project ID from response
|
|
384
|
+
let projectId;
|
|
385
|
+
if (typeof data.cloudaicompanionProject === 'string') {
|
|
386
|
+
projectId = data.cloudaicompanionProject;
|
|
387
|
+
}
|
|
388
|
+
else if (typeof data.cloudaicompanionProject === 'object') {
|
|
389
|
+
projectId = data.cloudaicompanionProject?.id;
|
|
262
390
|
}
|
|
391
|
+
if (!projectId?.trim()) {
|
|
392
|
+
return {
|
|
393
|
+
projectId: null,
|
|
394
|
+
error: 'Sign in to Antigravity app to activate quota.',
|
|
395
|
+
errorCode: 'account_unprovisioned',
|
|
396
|
+
actionHint: 'Complete sign-in in the Antigravity app, then retry quota refresh.',
|
|
397
|
+
isUnprovisioned: true,
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
// Extract tier - paidTier reflects actual subscription status, takes priority
|
|
401
|
+
const tierStr = data.paidTier?.id || data.currentTier?.id;
|
|
402
|
+
const tier = mapTierString(tierStr);
|
|
403
|
+
return { projectId: projectId.trim(), tier };
|
|
263
404
|
}
|
|
264
405
|
/**
|
|
265
406
|
* Fetch available models with quota info
|
|
266
407
|
* Note: projectId is kept for potential future use but not sent in body
|
|
267
408
|
* (CLIProxyAPI sends empty {} body for this endpoint)
|
|
268
409
|
*/
|
|
269
|
-
async function fetchAvailableModels(accessToken, _projectId) {
|
|
410
|
+
async function fetchAvailableModels(accountId, accessToken, _projectId) {
|
|
270
411
|
const url = `${ANTIGRAVITY_API_BASE}/${ANTIGRAVITY_API_VERSION}:fetchAvailableModels`;
|
|
271
|
-
const
|
|
272
|
-
|
|
273
|
-
try {
|
|
274
|
-
// Match CLIProxyAPI exactly: empty body, minimal headers
|
|
275
|
-
const response = await fetch(url, {
|
|
276
|
-
method: 'POST',
|
|
277
|
-
signal: controller.signal,
|
|
278
|
-
headers: {
|
|
279
|
-
...FETCHMODELS_HEADERS,
|
|
280
|
-
Authorization: `Bearer ${accessToken}`,
|
|
281
|
-
},
|
|
282
|
-
body: JSON.stringify({}),
|
|
283
|
-
});
|
|
284
|
-
clearTimeout(timeoutId);
|
|
285
|
-
if (response.status === 403) {
|
|
286
|
-
// 403 = account lacks Gemini Code Assist access (not same as quota exhausted)
|
|
287
|
-
// Keep success=false with isForbidden flag for UI to show distinct "403" badge
|
|
288
|
-
return {
|
|
289
|
-
success: false,
|
|
290
|
-
models: [],
|
|
291
|
-
lastUpdated: Date.now(),
|
|
292
|
-
isForbidden: true,
|
|
293
|
-
error: '403 Forbidden - No Gemini Code Assist access',
|
|
294
|
-
};
|
|
295
|
-
}
|
|
296
|
-
if (response.status === 401) {
|
|
297
|
-
return {
|
|
298
|
-
success: false,
|
|
299
|
-
models: [],
|
|
300
|
-
lastUpdated: Date.now(),
|
|
301
|
-
error: 'Access token expired or invalid',
|
|
302
|
-
};
|
|
303
|
-
}
|
|
304
|
-
if (response.status === 429) {
|
|
305
|
-
return {
|
|
306
|
-
success: false,
|
|
307
|
-
models: [],
|
|
308
|
-
lastUpdated: Date.now(),
|
|
309
|
-
error: 'Rate limited - try again later',
|
|
310
|
-
};
|
|
311
|
-
}
|
|
312
|
-
if (!response.ok) {
|
|
313
|
-
return {
|
|
314
|
-
success: false,
|
|
315
|
-
models: [],
|
|
316
|
-
lastUpdated: Date.now(),
|
|
317
|
-
error: `API error: ${response.status}`,
|
|
318
|
-
};
|
|
319
|
-
}
|
|
320
|
-
const data = (await response.json());
|
|
321
|
-
const models = [];
|
|
322
|
-
if (data.models && typeof data.models === 'object') {
|
|
323
|
-
for (const [modelId, modelData] of Object.entries(data.models)) {
|
|
324
|
-
const quotaInfo = modelData.quotaInfo || modelData.quota_info;
|
|
325
|
-
if (!quotaInfo)
|
|
326
|
-
continue;
|
|
327
|
-
// Extract remaining fraction (0-1 range)
|
|
328
|
-
const remaining = quotaInfo.remainingFraction ?? quotaInfo.remaining_fraction ?? quotaInfo.remaining;
|
|
329
|
-
// Extract reset time
|
|
330
|
-
const resetTime = quotaInfo.resetTime || quotaInfo.reset_time || null;
|
|
331
|
-
// If remaining is not a valid number but resetTime exists, treat as exhausted (0%)
|
|
332
|
-
// This happens when Claude models hit quota limit - API returns resetTime but no fraction
|
|
333
|
-
let percentage;
|
|
334
|
-
if (typeof remaining === 'number' && isFinite(remaining)) {
|
|
335
|
-
percentage = Math.max(0, Math.min(100, Math.round(remaining * 100)));
|
|
336
|
-
}
|
|
337
|
-
else if (resetTime) {
|
|
338
|
-
// Model is exhausted but has reset time - show as 0%
|
|
339
|
-
percentage = 0;
|
|
340
|
-
}
|
|
341
|
-
else {
|
|
342
|
-
// No valid data, skip this model
|
|
343
|
-
continue;
|
|
344
|
-
}
|
|
345
|
-
models.push({
|
|
346
|
-
name: modelId,
|
|
347
|
-
displayName: modelData.displayName,
|
|
348
|
-
percentage,
|
|
349
|
-
resetTime,
|
|
350
|
-
});
|
|
351
|
-
}
|
|
352
|
-
}
|
|
412
|
+
const response = await performAntigravityRequest(accountId, accessToken, url, FETCHMODELS_HEADERS, JSON.stringify({}));
|
|
413
|
+
if (response.status < 200 || response.status >= 300) {
|
|
353
414
|
return {
|
|
354
|
-
success:
|
|
355
|
-
models,
|
|
415
|
+
success: false,
|
|
416
|
+
models: [],
|
|
356
417
|
lastUpdated: Date.now(),
|
|
418
|
+
...buildAntigravityFailure(response.status, response.bodyText),
|
|
357
419
|
};
|
|
358
420
|
}
|
|
359
|
-
|
|
360
|
-
|
|
421
|
+
const data = response.json;
|
|
422
|
+
if (!data) {
|
|
361
423
|
return {
|
|
362
424
|
success: false,
|
|
363
425
|
models: [],
|
|
364
426
|
lastUpdated: Date.now(),
|
|
365
|
-
error:
|
|
427
|
+
error: 'Invalid quota response from provider',
|
|
428
|
+
errorCode: 'provider_unavailable',
|
|
429
|
+
retryable: true,
|
|
366
430
|
};
|
|
367
431
|
}
|
|
432
|
+
const models = [];
|
|
433
|
+
if (data.models && typeof data.models === 'object') {
|
|
434
|
+
for (const [modelId, modelData] of Object.entries(data.models)) {
|
|
435
|
+
const quotaInfo = modelData.quotaInfo || modelData.quota_info;
|
|
436
|
+
if (!quotaInfo)
|
|
437
|
+
continue;
|
|
438
|
+
const remaining = quotaInfo.remainingFraction ?? quotaInfo.remaining_fraction ?? quotaInfo.remaining;
|
|
439
|
+
const resetTime = quotaInfo.resetTime || quotaInfo.reset_time || null;
|
|
440
|
+
let percentage;
|
|
441
|
+
if (typeof remaining === 'number' && isFinite(remaining)) {
|
|
442
|
+
percentage = Math.max(0, Math.min(100, Math.round(remaining * 100)));
|
|
443
|
+
}
|
|
444
|
+
else if (resetTime) {
|
|
445
|
+
percentage = 0;
|
|
446
|
+
}
|
|
447
|
+
else {
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
models.push({
|
|
451
|
+
name: modelId,
|
|
452
|
+
displayName: modelData.displayName,
|
|
453
|
+
percentage,
|
|
454
|
+
resetTime,
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
return {
|
|
459
|
+
success: true,
|
|
460
|
+
models,
|
|
461
|
+
lastUpdated: Date.now(),
|
|
462
|
+
};
|
|
368
463
|
}
|
|
369
464
|
/**
|
|
370
465
|
* Fetch quota for an Antigravity account
|
|
@@ -400,56 +495,44 @@ async function fetchAccountQuota(provider, accountId, verbose = false) {
|
|
|
400
495
|
models: [],
|
|
401
496
|
lastUpdated: Date.now(),
|
|
402
497
|
error,
|
|
498
|
+
errorCode: 'auth_file_missing',
|
|
499
|
+
actionHint: 'Reconnect this account so CCS can read a current auth token.',
|
|
403
500
|
};
|
|
404
501
|
}
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
!authData.expiresAt || // No expiry info - refresh to be safe
|
|
414
|
-
new Date(authData.expiresAt).getTime() - Date.now() < REFRESH_LEAD_TIME_MS; // Expiring soon
|
|
415
|
-
if (shouldRefresh) {
|
|
416
|
-
const refreshResult = await refreshAccessToken(authData.refreshToken, verbose);
|
|
417
|
-
if (refreshResult.accessToken) {
|
|
418
|
-
accessToken = refreshResult.accessToken;
|
|
419
|
-
tokenRefreshed = true;
|
|
420
|
-
}
|
|
421
|
-
// If refresh fails, fall back to existing token (might still work)
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
if (verbose && !tokenRefreshed) {
|
|
425
|
-
console.error('[i] Token refresh: skipped');
|
|
502
|
+
const accessToken = authData.accessToken;
|
|
503
|
+
if (verbose) {
|
|
504
|
+
const expiryState = authData.isExpired
|
|
505
|
+
? 'expired'
|
|
506
|
+
: authData.expiresAt
|
|
507
|
+
? `expires ${authData.expiresAt}`
|
|
508
|
+
: 'expiry unknown';
|
|
509
|
+
console.error(`[i] Auth token state: ${expiryState}`);
|
|
426
510
|
}
|
|
427
511
|
// Get project ID and tier - prefer stored project ID, but always call API for tier
|
|
428
512
|
let projectId = authData.projectId;
|
|
429
513
|
let apiTier = 'unknown';
|
|
430
|
-
// Always call loadCodeAssist to get accurate tier from API
|
|
431
|
-
|
|
514
|
+
// Always call loadCodeAssist to get accurate tier from API.
|
|
515
|
+
// If the file token is stale, the helper retries through CLIProxy management auth.
|
|
516
|
+
const lastProjectResult = await getProjectId(accountId, accessToken);
|
|
432
517
|
if (!lastProjectResult.projectId && !projectId) {
|
|
433
|
-
|
|
434
|
-
if (
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
};
|
|
452
|
-
}
|
|
518
|
+
const error = lastProjectResult.error || 'Failed to retrieve project ID';
|
|
519
|
+
if (verbose)
|
|
520
|
+
console.error(`[!] Error: ${error}`);
|
|
521
|
+
return {
|
|
522
|
+
success: false,
|
|
523
|
+
models: [],
|
|
524
|
+
lastUpdated: Date.now(),
|
|
525
|
+
error,
|
|
526
|
+
errorCode: lastProjectResult.errorCode,
|
|
527
|
+
errorDetail: lastProjectResult.errorDetail,
|
|
528
|
+
actionHint: lastProjectResult.actionHint,
|
|
529
|
+
retryable: lastProjectResult.retryable,
|
|
530
|
+
httpStatus: lastProjectResult.httpStatus,
|
|
531
|
+
needsReauth: lastProjectResult.needsReauth,
|
|
532
|
+
isUnprovisioned: lastProjectResult.isUnprovisioned,
|
|
533
|
+
isExpired: authData.isExpired,
|
|
534
|
+
expiresAt: authData.expiresAt || undefined,
|
|
535
|
+
};
|
|
453
536
|
}
|
|
454
537
|
// Use API project ID if available, else fallback to stored
|
|
455
538
|
projectId = lastProjectResult.projectId || projectId;
|
|
@@ -457,38 +540,23 @@ async function fetchAccountQuota(provider, accountId, verbose = false) {
|
|
|
457
540
|
if (verbose)
|
|
458
541
|
console.error(`[i] Project ID: ${projectId || 'not found'}`);
|
|
459
542
|
// Fetch models with quota
|
|
460
|
-
const result = await fetchAvailableModels(accessToken, projectId);
|
|
543
|
+
const result = await fetchAvailableModels(accountId, accessToken, projectId);
|
|
461
544
|
if (verbose)
|
|
462
545
|
console.error(`[i] Models found: ${result.models.length}`);
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
const refreshResult = await refreshAccessToken(authData.refreshToken, verbose);
|
|
466
|
-
if (refreshResult.accessToken) {
|
|
467
|
-
const retryResult = await fetchAvailableModels(refreshResult.accessToken, projectId);
|
|
468
|
-
// Determine tier from API response only (model inference is unreliable)
|
|
469
|
-
if (retryResult.success) {
|
|
470
|
-
const finalTier = apiTier !== 'unknown' ? apiTier : 'unknown';
|
|
471
|
-
retryResult.tier = finalTier;
|
|
472
|
-
retryResult.accountId = accountId;
|
|
473
|
-
if (finalTier !== 'unknown') {
|
|
474
|
-
(0, account_manager_1.setAccountTier)(provider, accountId, finalTier);
|
|
475
|
-
}
|
|
476
|
-
if (verbose && retryResult.error) {
|
|
477
|
-
console.log(`[!] Error: ${retryResult.error}`);
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
return retryResult;
|
|
481
|
-
}
|
|
482
|
-
}
|
|
546
|
+
result.accountId = accountId;
|
|
547
|
+
result.projectId = projectId || undefined;
|
|
483
548
|
// Determine tier from API response only
|
|
484
549
|
if (result.success) {
|
|
485
550
|
const finalTier = apiTier !== 'unknown' ? apiTier : 'unknown';
|
|
486
551
|
result.tier = finalTier;
|
|
487
|
-
result.accountId = accountId;
|
|
488
552
|
if (finalTier !== 'unknown') {
|
|
489
553
|
(0, account_manager_1.setAccountTier)(provider, accountId, finalTier);
|
|
490
554
|
}
|
|
491
555
|
}
|
|
556
|
+
else {
|
|
557
|
+
result.isExpired = authData.isExpired;
|
|
558
|
+
result.expiresAt = authData.expiresAt || undefined;
|
|
559
|
+
}
|
|
492
560
|
if (verbose && result.error) {
|
|
493
561
|
console.log(`[!] Error: ${result.error}`);
|
|
494
562
|
}
|