ag-quota 0.0.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/LICENSE +15 -0
- package/README.md +115 -0
- package/bin/ag-quota.js +466 -0
- package/dist/cli-auth.d.ts +12 -0
- package/dist/cli-auth.d.ts.map +1 -0
- package/dist/cli-auth.js +89 -0
- package/dist/cloud.d.ts +40 -0
- package/dist/cloud.d.ts.map +1 -0
- package/dist/cloud.js +110 -0
- package/dist/config.d.ts +101 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +88 -0
- package/dist/index.d.ts +75 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +248 -0
- package/package.json +57 -0
- package/src/cli-auth.ts +136 -0
- package/src/cli.ts +154 -0
- package/src/cloud.ts +195 -0
- package/src/config.ts +193 -0
- package/src/index.test.ts +96 -0
- package/src/index.ts +379 -0
package/dist/cloud.d.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified quota info structure (shared between cloud and local)
|
|
3
|
+
*/
|
|
4
|
+
export interface QuotaInfo {
|
|
5
|
+
remainingFraction: number;
|
|
6
|
+
resetTime?: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Unified model config structure (shared between cloud and local)
|
|
10
|
+
*/
|
|
11
|
+
export interface ModelConfig {
|
|
12
|
+
modelName: string;
|
|
13
|
+
label?: string;
|
|
14
|
+
quotaInfo?: QuotaInfo;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Cloud-specific account info
|
|
18
|
+
*/
|
|
19
|
+
export interface CloudAccountInfo {
|
|
20
|
+
email?: string;
|
|
21
|
+
projectId?: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Result from cloud quota fetch
|
|
25
|
+
*/
|
|
26
|
+
export interface CloudQuotaResult {
|
|
27
|
+
account: CloudAccountInfo;
|
|
28
|
+
models: ModelConfig[];
|
|
29
|
+
timestamp: number;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Fetch quota information from the Cloud Code API.
|
|
33
|
+
*
|
|
34
|
+
* @param accessToken - Valid OAuth2 access token
|
|
35
|
+
* @param projectId - Optional Google Cloud project ID
|
|
36
|
+
* @returns CloudQuotaResult with account info and model quotas
|
|
37
|
+
* @throws Error if API fails
|
|
38
|
+
*/
|
|
39
|
+
export declare function fetchCloudQuota(accessToken: string, projectId?: string): Promise<CloudQuotaResult>;
|
|
40
|
+
//# sourceMappingURL=cloud.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cloud.d.ts","sourceRoot":"","sources":["../src/cloud.ts"],"names":[],"mappings":"AAkDA;;GAEG;AACH,MAAM,WAAW,SAAS;IACtB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;GAEG;AACH,MAAM,WAAW,WAAW;IACxB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,SAAS,CAAC;CACzB;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC7B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC7B,OAAO,EAAE,gBAAgB,CAAC;IAC1B,MAAM,EAAE,WAAW,EAAE,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;CACrB;AAoED;;;;;;;GAOG;AACH,wBAAsB,eAAe,CACjC,WAAW,EAAE,MAAM,EACnB,SAAS,CAAC,EAAE,MAAM,GACnB,OAAO,CAAC,gBAAgB,CAAC,CAiC3B"}
|
package/dist/cloud.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* ISC License
|
|
3
|
+
* Copyright (c) 2025, Cristian Militaru
|
|
4
|
+
* Copyright (c) 2026, Philipp
|
|
5
|
+
*
|
|
6
|
+
* Cloud quota fetching via Google Cloud Code API.
|
|
7
|
+
*/
|
|
8
|
+
// ============================================================================
|
|
9
|
+
// Constants from opencode-antigravity-auth
|
|
10
|
+
// ============================================================================
|
|
11
|
+
// Endpoint fallback order (daily → autopush → prod)
|
|
12
|
+
const CLOUDCODE_ENDPOINTS = [
|
|
13
|
+
"https://daily-cloudcode-pa.sandbox.googleapis.com",
|
|
14
|
+
"https://autopush-cloudcode-pa.sandbox.googleapis.com",
|
|
15
|
+
"https://cloudcode-pa.googleapis.com",
|
|
16
|
+
];
|
|
17
|
+
// Headers matching opencode-antigravity-auth
|
|
18
|
+
const CLOUDCODE_HEADERS = {
|
|
19
|
+
"Content-Type": "application/json",
|
|
20
|
+
"User-Agent": "antigravity/1.11.5 windows/amd64",
|
|
21
|
+
"X-Goog-Api-Client": "google-cloud-sdk vscode_cloudshelleditor/0.1",
|
|
22
|
+
"Client-Metadata": '{"ideType":"IDE_UNSPECIFIED","platform":"PLATFORM_UNSPECIFIED","pluginType":"GEMINI"}',
|
|
23
|
+
};
|
|
24
|
+
// ============================================================================
|
|
25
|
+
// API Fetching
|
|
26
|
+
// ============================================================================
|
|
27
|
+
/**
|
|
28
|
+
* Fetch available models with quota from the Cloud Code API.
|
|
29
|
+
*/
|
|
30
|
+
async function fetchAvailableModels(accessToken, projectId) {
|
|
31
|
+
const payload = projectId ? { project: projectId } : {};
|
|
32
|
+
let lastError = null;
|
|
33
|
+
const headers = {
|
|
34
|
+
...CLOUDCODE_HEADERS,
|
|
35
|
+
Authorization: `Bearer ${accessToken}`,
|
|
36
|
+
};
|
|
37
|
+
for (const endpoint of CLOUDCODE_ENDPOINTS) {
|
|
38
|
+
try {
|
|
39
|
+
const url = `${endpoint}/v1internal:fetchAvailableModels`;
|
|
40
|
+
const response = await fetch(url, {
|
|
41
|
+
method: "POST",
|
|
42
|
+
headers,
|
|
43
|
+
body: JSON.stringify(payload),
|
|
44
|
+
});
|
|
45
|
+
if (response.status === 401) {
|
|
46
|
+
throw new Error("Authorization expired or invalid.");
|
|
47
|
+
}
|
|
48
|
+
if (response.status === 403) {
|
|
49
|
+
throw new Error("Access forbidden (403). Check your account permissions.");
|
|
50
|
+
}
|
|
51
|
+
if (!response.ok) {
|
|
52
|
+
const text = await response.text();
|
|
53
|
+
throw new Error(`Cloud Code API error ${response.status}: ${text.slice(0, 200)}`);
|
|
54
|
+
}
|
|
55
|
+
return (await response.json());
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
59
|
+
// Auth errors should not fallback to other endpoints
|
|
60
|
+
if (lastError.message.includes("Authorization") ||
|
|
61
|
+
lastError.message.includes("forbidden") ||
|
|
62
|
+
lastError.message.includes("invalid_grant")) {
|
|
63
|
+
throw lastError;
|
|
64
|
+
}
|
|
65
|
+
// Try next endpoint
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
throw lastError || new Error("All Cloud Code API endpoints failed");
|
|
69
|
+
}
|
|
70
|
+
// ============================================================================
|
|
71
|
+
// Main Export
|
|
72
|
+
// ============================================================================
|
|
73
|
+
/**
|
|
74
|
+
* Fetch quota information from the Cloud Code API.
|
|
75
|
+
*
|
|
76
|
+
* @param accessToken - Valid OAuth2 access token
|
|
77
|
+
* @param projectId - Optional Google Cloud project ID
|
|
78
|
+
* @returns CloudQuotaResult with account info and model quotas
|
|
79
|
+
* @throws Error if API fails
|
|
80
|
+
*/
|
|
81
|
+
export async function fetchCloudQuota(accessToken, projectId) {
|
|
82
|
+
if (!accessToken) {
|
|
83
|
+
throw new Error("Access token is required for cloud quota fetching");
|
|
84
|
+
}
|
|
85
|
+
// Fetch quota data
|
|
86
|
+
const response = await fetchAvailableModels(accessToken, projectId);
|
|
87
|
+
// Convert to unified format
|
|
88
|
+
const models = [];
|
|
89
|
+
if (response.models) {
|
|
90
|
+
for (const [modelKey, info] of Object.entries(response.models)) {
|
|
91
|
+
if (!info.quotaInfo)
|
|
92
|
+
continue;
|
|
93
|
+
models.push({
|
|
94
|
+
modelName: info.model || modelKey,
|
|
95
|
+
label: info.displayName || modelKey,
|
|
96
|
+
quotaInfo: {
|
|
97
|
+
remainingFraction: info.quotaInfo.remainingFraction ?? 0,
|
|
98
|
+
resetTime: info.quotaInfo.resetTime,
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
account: {
|
|
105
|
+
projectId,
|
|
106
|
+
},
|
|
107
|
+
models,
|
|
108
|
+
timestamp: Date.now(),
|
|
109
|
+
};
|
|
110
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quota data source
|
|
3
|
+
* - "cloud": Fetch from Cloud Code API (requires opencode-antigravity-auth)
|
|
4
|
+
* - "local": Fetch from local language server process
|
|
5
|
+
* - "auto": Try cloud first, fallback to local
|
|
6
|
+
*/
|
|
7
|
+
export type QuotaSource = "cloud" | "local" | "auto";
|
|
8
|
+
export interface QuotaIndicator {
|
|
9
|
+
threshold: number;
|
|
10
|
+
symbol: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Configuration options for the quota plugin
|
|
14
|
+
*/
|
|
15
|
+
export interface QuotaConfig {
|
|
16
|
+
/**
|
|
17
|
+
* Where to fetch quota data from.
|
|
18
|
+
* - "cloud": Use Cloud Code API (requires opencode-antigravity-auth)
|
|
19
|
+
* - "local": Use local language server process
|
|
20
|
+
* - "auto": Try cloud first, fallback to local (default)
|
|
21
|
+
* @default "auto"
|
|
22
|
+
*/
|
|
23
|
+
quotaSource?: QuotaSource;
|
|
24
|
+
/**
|
|
25
|
+
* Format string for quota display.
|
|
26
|
+
* Available placeholders:
|
|
27
|
+
* - {category} - Category name (Flash, Pro, Claude/GPT)
|
|
28
|
+
* - {percent} - Quota percentage (e.g., "85.5")
|
|
29
|
+
* - {resetIn} - Relative time until reset (e.g., "2h 30m")
|
|
30
|
+
* - {resetAt} - Absolute reset time (e.g., "10:30 PM")
|
|
31
|
+
* - {model} - Current model ID
|
|
32
|
+
*
|
|
33
|
+
* For all quotas mode, the format is applied per category and joined with separator.
|
|
34
|
+
* @default "{category}: {percent}% ({resetIn})"
|
|
35
|
+
*/
|
|
36
|
+
format?: string;
|
|
37
|
+
/**
|
|
38
|
+
* Separator between categories when showing all quotas
|
|
39
|
+
* @default " | "
|
|
40
|
+
*/
|
|
41
|
+
separator?: string;
|
|
42
|
+
/**
|
|
43
|
+
* Show all quota categories or only the current model's quota
|
|
44
|
+
* @default "all"
|
|
45
|
+
*/
|
|
46
|
+
displayMode?: "all" | "current";
|
|
47
|
+
/**
|
|
48
|
+
* Always append quota info, even when unavailable
|
|
49
|
+
* @default true
|
|
50
|
+
*/
|
|
51
|
+
alwaysAppend?: boolean;
|
|
52
|
+
/**
|
|
53
|
+
* The marker string used to separate quota info from the message
|
|
54
|
+
* @default "> AG Quota:"
|
|
55
|
+
*/
|
|
56
|
+
quotaMarker?: string;
|
|
57
|
+
/**
|
|
58
|
+
* Polling interval in milliseconds
|
|
59
|
+
* @default 30000 (30 seconds)
|
|
60
|
+
*/
|
|
61
|
+
pollingInterval?: number;
|
|
62
|
+
/**
|
|
63
|
+
* Array of quota usage percentages (remaining) that trigger alerts
|
|
64
|
+
* @default [0.5, 0.1, 0.05] (50%, 10%, 5%)
|
|
65
|
+
*/
|
|
66
|
+
alertThresholds?: number[];
|
|
67
|
+
/**
|
|
68
|
+
* Visual indicators appended to quota percentages when remaining fraction is low.
|
|
69
|
+
* The most severe matching indicator is chosen.
|
|
70
|
+
*
|
|
71
|
+
* Example: with indicators [{threshold: 0.1, symbol: "⚠️"}, {threshold: 0.05, symbol: "⛔"}]
|
|
72
|
+
* a remainingFraction of 0.04 will show "⛔".
|
|
73
|
+
*
|
|
74
|
+
* @default [{threshold: 0.1, symbol: "⚠️"}, {threshold: 0.05, symbol: "⛔"}]
|
|
75
|
+
*/
|
|
76
|
+
indicators?: QuotaIndicator[];
|
|
77
|
+
}
|
|
78
|
+
declare const DEFAULT_CONFIG: Required<QuotaConfig>;
|
|
79
|
+
/**
|
|
80
|
+
* Load configuration from file system.
|
|
81
|
+
* Searches in order:
|
|
82
|
+
* 1. .opencode/ag-quota.json (project-local)
|
|
83
|
+
* 2. ~/.config/opencode/ag-quota.json (user global)
|
|
84
|
+
*
|
|
85
|
+
* @param projectDir - The project directory to search from
|
|
86
|
+
* @returns Merged configuration with defaults
|
|
87
|
+
*/
|
|
88
|
+
export declare function loadConfig(projectDir?: string): Promise<Required<QuotaConfig>>;
|
|
89
|
+
/**
|
|
90
|
+
* Format a quota entry using the format string.
|
|
91
|
+
* Placeholders are only replaced if they exist in the format string.
|
|
92
|
+
*/
|
|
93
|
+
export declare function formatQuotaEntry(format: string, data: {
|
|
94
|
+
category: string;
|
|
95
|
+
percent: string;
|
|
96
|
+
resetIn: string | null;
|
|
97
|
+
resetAt: string | null;
|
|
98
|
+
model: string;
|
|
99
|
+
}): string;
|
|
100
|
+
export { DEFAULT_CONFIG };
|
|
101
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AASA;;;;;GAKG;AACH,MAAM,MAAM,WAAW,GAAG,OAAO,GAAG,OAAO,GAAG,MAAM,CAAC;AAErD,MAAM,WAAW,cAAc;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,WAAW;IACxB;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,WAAW,CAAC;IAE1B;;;;;;;;;;;OAWG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;;OAGG;IACH,WAAW,CAAC,EAAE,KAAK,GAAG,SAAS,CAAC;IAEhC;;;OAGG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IAEvB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IAEzB;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;IAE3B;;;;;;;;OAQG;IACH,UAAU,CAAC,EAAE,cAAc,EAAE,CAAC;CACjC;AAED,QAAA,MAAM,cAAc,EAAE,QAAQ,CAAC,WAAW,CAazC,CAAC;AAEF;;;;;;;;GAQG;AACH,wBAAsB,UAAU,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC,CAyBpF;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAC5B,MAAM,EAAE,MAAM,EACd,IAAI,EAAE;IACF,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;CACjB,GACF,MAAM,CA6BR;AAED,OAAO,EAAE,cAAc,EAAE,CAAC"}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* ISC License
|
|
3
|
+
* Copyright (c) 2026 Philipp
|
|
4
|
+
*/
|
|
5
|
+
import { readFile } from "node:fs/promises";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
const DEFAULT_CONFIG = {
|
|
9
|
+
quotaSource: "auto",
|
|
10
|
+
format: "{category}: {percent}% ({resetIn})",
|
|
11
|
+
separator: " | ",
|
|
12
|
+
displayMode: "all",
|
|
13
|
+
alwaysAppend: true,
|
|
14
|
+
quotaMarker: "> AG Quota:",
|
|
15
|
+
pollingInterval: 30000,
|
|
16
|
+
alertThresholds: [0.5, 0.1, 0.05],
|
|
17
|
+
indicators: [
|
|
18
|
+
{ threshold: 0.2, symbol: "⚠️" },
|
|
19
|
+
{ threshold: 0.05, symbol: "🛑" },
|
|
20
|
+
],
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Load configuration from file system.
|
|
24
|
+
* Searches in order:
|
|
25
|
+
* 1. .opencode/ag-quota.json (project-local)
|
|
26
|
+
* 2. ~/.config/opencode/ag-quota.json (user global)
|
|
27
|
+
*
|
|
28
|
+
* @param projectDir - The project directory to search from
|
|
29
|
+
* @returns Merged configuration with defaults
|
|
30
|
+
*/
|
|
31
|
+
export async function loadConfig(projectDir) {
|
|
32
|
+
const paths = [];
|
|
33
|
+
// Project-local config
|
|
34
|
+
if (projectDir) {
|
|
35
|
+
paths.push(join(projectDir, ".opencode", "ag-quota.json"));
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
paths.push(join(process.cwd(), ".opencode", "ag-quota.json"));
|
|
39
|
+
}
|
|
40
|
+
// User global config
|
|
41
|
+
paths.push(join(homedir(), ".config", "opencode", "ag-quota.json"));
|
|
42
|
+
for (const configPath of paths) {
|
|
43
|
+
try {
|
|
44
|
+
const content = await readFile(configPath, "utf-8");
|
|
45
|
+
const userConfig = JSON.parse(content);
|
|
46
|
+
return { ...DEFAULT_CONFIG, ...userConfig };
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
// File doesn't exist or is invalid, try next
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return DEFAULT_CONFIG;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Format a quota entry using the format string.
|
|
57
|
+
* Placeholders are only replaced if they exist in the format string.
|
|
58
|
+
*/
|
|
59
|
+
export function formatQuotaEntry(format, data) {
|
|
60
|
+
let result = format
|
|
61
|
+
.replace("{category}", data.category)
|
|
62
|
+
.replace("{percent}", data.percent)
|
|
63
|
+
.replace("{model}", data.model);
|
|
64
|
+
// Handle resetIn (relative time) - e.g., "2h 30m"
|
|
65
|
+
if (format.includes("{resetIn}")) {
|
|
66
|
+
if (data.resetIn) {
|
|
67
|
+
result = result.replace("{resetIn}", data.resetIn);
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
result = result
|
|
71
|
+
.replace(/\s*\(\{resetIn}\)/, "")
|
|
72
|
+
.replace(/\s*\{resetIn}/, "");
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// Handle resetAt (absolute time) - e.g., "10:30 PM"
|
|
76
|
+
if (format.includes("{resetAt}")) {
|
|
77
|
+
if (data.resetAt) {
|
|
78
|
+
result = result.replace("{resetAt}", data.resetAt);
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
result = result
|
|
82
|
+
.replace(/\s*\(\{resetAt}\)/, "")
|
|
83
|
+
.replace(/\s*\{resetAt}/, "");
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return result;
|
|
87
|
+
}
|
|
88
|
+
export { DEFAULT_CONFIG };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
export declare const API_ENDPOINTS: {
|
|
2
|
+
GET_USER_STATUS: string;
|
|
3
|
+
};
|
|
4
|
+
export interface QuotaInfo {
|
|
5
|
+
remainingFraction: number;
|
|
6
|
+
resetTime?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface ModelConfig {
|
|
9
|
+
modelName: string;
|
|
10
|
+
label?: string;
|
|
11
|
+
quotaInfo?: QuotaInfo;
|
|
12
|
+
}
|
|
13
|
+
export interface UserStatus {
|
|
14
|
+
cascadeModelConfigData?: {
|
|
15
|
+
clientModelConfigs?: ModelConfig[];
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export interface UserStatusResponse {
|
|
19
|
+
userStatus: UserStatus;
|
|
20
|
+
timestamp: number;
|
|
21
|
+
}
|
|
22
|
+
export type ShellRunner = (cmd: string) => Promise<string>;
|
|
23
|
+
export declare function fetchAntigravityStatus(runShell: ShellRunner): Promise<UserStatusResponse>;
|
|
24
|
+
/**
|
|
25
|
+
* Format relative time until target date (e.g., "2h 30m")
|
|
26
|
+
*/
|
|
27
|
+
export declare function formatRelativeTime(targetDate: Date): string;
|
|
28
|
+
/**
|
|
29
|
+
* Format absolute time of target date (e.g., "10:30 PM" or "22:30")
|
|
30
|
+
*/
|
|
31
|
+
export declare function formatAbsoluteTime(targetDate: Date): string;
|
|
32
|
+
export { loadConfig, formatQuotaEntry, DEFAULT_CONFIG, type QuotaConfig, type QuotaSource, } from "./config";
|
|
33
|
+
export { fetchCloudQuota, type CloudQuotaResult, type CloudAccountInfo, } from "./cloud";
|
|
34
|
+
/**
|
|
35
|
+
* Unified category quota info (for the three model groups)
|
|
36
|
+
*/
|
|
37
|
+
export interface CategoryQuota {
|
|
38
|
+
category: "Flash" | "Pro" | "Claude/GPT";
|
|
39
|
+
remainingFraction: number;
|
|
40
|
+
resetTime: Date | null;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Unified quota result from either source
|
|
44
|
+
*/
|
|
45
|
+
export interface UnifiedQuotaResult {
|
|
46
|
+
source: "cloud" | "local";
|
|
47
|
+
categories: CategoryQuota[];
|
|
48
|
+
models: ModelConfig[];
|
|
49
|
+
timestamp: number;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Credentials for cloud quota fetching
|
|
53
|
+
*/
|
|
54
|
+
export interface CloudAuthCredentials {
|
|
55
|
+
accessToken: string;
|
|
56
|
+
projectId?: string;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Categorize a model label into one of the three groups.
|
|
60
|
+
*/
|
|
61
|
+
export declare function categorizeModel(label: string): "Flash" | "Pro" | "Claude/GPT";
|
|
62
|
+
/**
|
|
63
|
+
* Group models into the three categories, taking the minimum quota per category.
|
|
64
|
+
*/
|
|
65
|
+
export declare function groupModelsByCategory(models: ModelConfig[]): CategoryQuota[];
|
|
66
|
+
/**
|
|
67
|
+
* Fetch quota from either cloud or local source.
|
|
68
|
+
*
|
|
69
|
+
* @param source - "cloud", "local", or "auto" (try cloud first, fallback to local)
|
|
70
|
+
* @param shellRunner - Required for local source
|
|
71
|
+
* @param cloudAuth - Required for cloud source
|
|
72
|
+
* @returns Unified quota result
|
|
73
|
+
*/
|
|
74
|
+
export declare function fetchQuota(source: "cloud" | "local" | "auto", shellRunner?: ShellRunner, cloudAuth?: CloudAuthCredentials): Promise<UnifiedQuotaResult>;
|
|
75
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAqBA,eAAO,MAAM,aAAa;;CAGzB,CAAC;AAEF,MAAM,WAAW,SAAS;IACtB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,WAAW;IACxB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,SAAS,CAAC;CACzB;AAED,MAAM,WAAW,UAAU;IACvB,sBAAsB,CAAC,EAAE;QACrB,kBAAkB,CAAC,EAAE,WAAW,EAAE,CAAC;KACtC,CAAC;CACL;AAED,MAAM,WAAW,kBAAkB;IAC/B,UAAU,EAAE,UAAU,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,MAAM,WAAW,GAAG,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;AAqD3D,wBAAsB,sBAAsB,CACxC,QAAQ,EAAE,WAAW,GACtB,OAAO,CAAC,kBAAkB,CAAC,CAqF7B;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,IAAI,GAAG,MAAM,CAa3D;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,IAAI,GAAG,MAAM,CAK3D;AAGD,OAAO,EACH,UAAU,EACV,gBAAgB,EAChB,cAAc,EACd,KAAK,WAAW,EAChB,KAAK,WAAW,GACnB,MAAM,UAAU,CAAC;AAGlB,OAAO,EACH,eAAe,EACf,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,GACxB,MAAM,SAAS,CAAC;AAMjB;;GAEG;AACH,MAAM,WAAW,aAAa;IAC1B,QAAQ,EAAE,OAAO,GAAG,KAAK,GAAG,YAAY,CAAC;IACzC,iBAAiB,EAAE,MAAM,CAAC;IAC1B,SAAS,EAAE,IAAI,GAAG,IAAI,CAAC;CAC1B;AAED;;GAEG;AACH,MAAM,WAAW,kBAAkB;IAC/B,MAAM,EAAE,OAAO,GAAG,OAAO,CAAC;IAC1B,UAAU,EAAE,aAAa,EAAE,CAAC;IAC5B,MAAM,EAAE,WAAW,EAAE,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;CACrB;AAED;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACjC,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,GAAG,KAAK,GAAG,YAAY,CAS7E;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,WAAW,EAAE,GAAG,aAAa,EAAE,CAkC5E;AAED;;;;;;;GAOG;AACH,wBAAsB,UAAU,CAC5B,MAAM,EAAE,OAAO,GAAG,OAAO,GAAG,MAAM,EAClC,WAAW,CAAC,EAAE,WAAW,EACzB,SAAS,CAAC,EAAE,oBAAoB,GACjC,OAAO,CAAC,kBAAkB,CAAC,CAgD7B"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/*
|
|
2
|
+
ISC License
|
|
3
|
+
|
|
4
|
+
Copyright (c) 2025, Cristian Militaru
|
|
5
|
+
|
|
6
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
7
|
+
purpose with or without fee is hereby granted, provided that the above
|
|
8
|
+
copyright notice and this permission notice appear in all copies.
|
|
9
|
+
|
|
10
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
11
|
+
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
12
|
+
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
13
|
+
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
14
|
+
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
15
|
+
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
16
|
+
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
17
|
+
*/
|
|
18
|
+
import * as http from "node:http";
|
|
19
|
+
import * as https from "node:https";
|
|
20
|
+
export const API_ENDPOINTS = {
|
|
21
|
+
GET_USER_STATUS: "/exa.language_server_pb.LanguageServerService/GetUserStatus",
|
|
22
|
+
};
|
|
23
|
+
function makeRequest(port, csrfToken, path, body) {
|
|
24
|
+
return new Promise((resolve, reject) => {
|
|
25
|
+
const payload = JSON.stringify(body);
|
|
26
|
+
const options = {
|
|
27
|
+
hostname: "127.0.0.1",
|
|
28
|
+
port,
|
|
29
|
+
path,
|
|
30
|
+
method: "POST",
|
|
31
|
+
headers: {
|
|
32
|
+
"Content-Type": "application/json",
|
|
33
|
+
"Content-Length": Buffer.byteLength(payload),
|
|
34
|
+
"X-Codeium-Csrf-Token": csrfToken,
|
|
35
|
+
"Connect-Protocol-Version": "1",
|
|
36
|
+
},
|
|
37
|
+
timeout: 2000,
|
|
38
|
+
};
|
|
39
|
+
const handleResponse = (response) => {
|
|
40
|
+
let data = "";
|
|
41
|
+
response.on("data", (chunk) => {
|
|
42
|
+
data += chunk.toString();
|
|
43
|
+
});
|
|
44
|
+
response.on("end", () => {
|
|
45
|
+
try {
|
|
46
|
+
resolve(JSON.parse(data));
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
reject(new Error("JSON parse error"));
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
};
|
|
53
|
+
const req = https.request({ ...options, rejectUnauthorized: false }, handleResponse);
|
|
54
|
+
req.on("error", () => {
|
|
55
|
+
const reqHttp = http.request(options, handleResponse);
|
|
56
|
+
reqHttp.on("error", (err) => reject(err));
|
|
57
|
+
reqHttp.write(payload);
|
|
58
|
+
reqHttp.end();
|
|
59
|
+
});
|
|
60
|
+
req.write(payload);
|
|
61
|
+
req.end();
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
export async function fetchAntigravityStatus(runShell) {
|
|
65
|
+
let procOutput = "";
|
|
66
|
+
try {
|
|
67
|
+
procOutput = await runShell('ps aux | grep -E "csrf_token|language_server" | grep -v grep');
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
procOutput = "";
|
|
71
|
+
}
|
|
72
|
+
const lines = procOutput.split("\n");
|
|
73
|
+
let csrfToken = "";
|
|
74
|
+
let cmdLinePort = 0;
|
|
75
|
+
for (const line of lines) {
|
|
76
|
+
const csrfMatch = line.match(/--csrf_token[=\s]+([\w-]+)/i);
|
|
77
|
+
if (csrfMatch?.[1])
|
|
78
|
+
csrfToken = csrfMatch[1];
|
|
79
|
+
const portMatch = line.match(/--extension_server_port[=\s]+(\d+)/i);
|
|
80
|
+
if (portMatch?.[1])
|
|
81
|
+
cmdLinePort = parseInt(portMatch[1], 10);
|
|
82
|
+
if (csrfToken && cmdLinePort)
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
if (!csrfToken) {
|
|
86
|
+
throw new Error("Antigravity CSRF token not found. Is the Language Server running?");
|
|
87
|
+
}
|
|
88
|
+
let netstatOutput = "";
|
|
89
|
+
try {
|
|
90
|
+
netstatOutput = await runShell('ss -tlnp | grep -E "language_server|opencode|node"');
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
netstatOutput = "";
|
|
94
|
+
}
|
|
95
|
+
const portMatches = netstatOutput.match(/:(\d+)/g);
|
|
96
|
+
let ports = portMatches
|
|
97
|
+
? portMatches.map((p) => parseInt(p.replace(":", ""), 10))
|
|
98
|
+
: [];
|
|
99
|
+
if (cmdLinePort && !ports.includes(cmdLinePort)) {
|
|
100
|
+
ports.unshift(cmdLinePort);
|
|
101
|
+
}
|
|
102
|
+
ports = Array.from(new Set(ports));
|
|
103
|
+
if (ports.length === 0) {
|
|
104
|
+
throw new Error("No listening ports found for Antigravity. Check if the server is active.");
|
|
105
|
+
}
|
|
106
|
+
let userStatus = null;
|
|
107
|
+
let lastError = null;
|
|
108
|
+
for (const p of ports) {
|
|
109
|
+
try {
|
|
110
|
+
const resp = await makeRequest(p, csrfToken, API_ENDPOINTS.GET_USER_STATUS, { metadata: { ideName: "opencode" } });
|
|
111
|
+
if (resp?.userStatus) {
|
|
112
|
+
userStatus = resp.userStatus;
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
catch (e) {
|
|
117
|
+
lastError = e instanceof Error ? e : new Error(String(e));
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (!userStatus) {
|
|
122
|
+
throw new Error(`Could not communicate with Antigravity API. ${lastError?.message ?? ""}`);
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
userStatus,
|
|
126
|
+
timestamp: Date.now(),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Format relative time until target date (e.g., "2h 30m")
|
|
131
|
+
*/
|
|
132
|
+
export function formatRelativeTime(targetDate) {
|
|
133
|
+
const now = new Date();
|
|
134
|
+
const diffMs = targetDate.getTime() - now.getTime();
|
|
135
|
+
if (diffMs <= 0)
|
|
136
|
+
return "now";
|
|
137
|
+
const diffMins = Math.floor(diffMs / (1000 * 60));
|
|
138
|
+
const diffHours = Math.floor(diffMins / 60);
|
|
139
|
+
const remainingMins = diffMins % 60;
|
|
140
|
+
if (diffHours > 0) {
|
|
141
|
+
return `${diffHours}h ${remainingMins}m`;
|
|
142
|
+
}
|
|
143
|
+
return `${diffMins}m`;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Format absolute time of target date (e.g., "10:30 PM" or "22:30")
|
|
147
|
+
*/
|
|
148
|
+
export function formatAbsoluteTime(targetDate) {
|
|
149
|
+
return targetDate.toLocaleTimeString(undefined, {
|
|
150
|
+
hour: "2-digit",
|
|
151
|
+
minute: "2-digit",
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
// Re-export config utilities
|
|
155
|
+
export { loadConfig, formatQuotaEntry, DEFAULT_CONFIG, } from "./config";
|
|
156
|
+
// Re-export cloud utilities
|
|
157
|
+
export { fetchCloudQuota, } from "./cloud";
|
|
158
|
+
/**
|
|
159
|
+
* Categorize a model label into one of the three groups.
|
|
160
|
+
*/
|
|
161
|
+
export function categorizeModel(label) {
|
|
162
|
+
const lowerLabel = label.toLowerCase();
|
|
163
|
+
if (lowerLabel.includes("flash")) {
|
|
164
|
+
return "Flash";
|
|
165
|
+
}
|
|
166
|
+
if (lowerLabel.includes("gemini") || lowerLabel.includes("pro")) {
|
|
167
|
+
return "Pro";
|
|
168
|
+
}
|
|
169
|
+
return "Claude/GPT";
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Group models into the three categories, taking the minimum quota per category.
|
|
173
|
+
*/
|
|
174
|
+
export function groupModelsByCategory(models) {
|
|
175
|
+
const categories = {};
|
|
176
|
+
for (const model of models) {
|
|
177
|
+
const label = model.label || model.modelName || "";
|
|
178
|
+
const category = categorizeModel(label);
|
|
179
|
+
const fraction = model.quotaInfo?.remainingFraction ?? 0;
|
|
180
|
+
const resetTime = model.quotaInfo?.resetTime
|
|
181
|
+
? new Date(model.quotaInfo.resetTime)
|
|
182
|
+
: null;
|
|
183
|
+
if (!categories[category] ||
|
|
184
|
+
fraction < categories[category].remainingFraction) {
|
|
185
|
+
categories[category] = { remainingFraction: fraction, resetTime };
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
const result = [];
|
|
189
|
+
for (const cat of ["Flash", "Pro", "Claude/GPT"]) {
|
|
190
|
+
if (categories[cat]) {
|
|
191
|
+
result.push({
|
|
192
|
+
category: cat,
|
|
193
|
+
remainingFraction: categories[cat].remainingFraction,
|
|
194
|
+
resetTime: categories[cat].resetTime,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return result;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Fetch quota from either cloud or local source.
|
|
202
|
+
*
|
|
203
|
+
* @param source - "cloud", "local", or "auto" (try cloud first, fallback to local)
|
|
204
|
+
* @param shellRunner - Required for local source
|
|
205
|
+
* @param cloudAuth - Required for cloud source
|
|
206
|
+
* @returns Unified quota result
|
|
207
|
+
*/
|
|
208
|
+
export async function fetchQuota(source, shellRunner, cloudAuth) {
|
|
209
|
+
// Import cloud module dynamically to avoid issues if not available
|
|
210
|
+
const { fetchCloudQuota } = await import("./cloud");
|
|
211
|
+
if (source === "cloud" || source === "auto") {
|
|
212
|
+
// Try cloud first
|
|
213
|
+
if (cloudAuth) {
|
|
214
|
+
try {
|
|
215
|
+
const cloudResult = await fetchCloudQuota(cloudAuth.accessToken, cloudAuth.projectId);
|
|
216
|
+
const categories = groupModelsByCategory(cloudResult.models);
|
|
217
|
+
return {
|
|
218
|
+
source: "cloud",
|
|
219
|
+
categories,
|
|
220
|
+
models: cloudResult.models,
|
|
221
|
+
timestamp: cloudResult.timestamp,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
catch (error) {
|
|
225
|
+
if (source === "cloud") {
|
|
226
|
+
throw error; // Don't fallback if explicitly requested cloud
|
|
227
|
+
}
|
|
228
|
+
// Fall through to local if auto
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
else if (source === "cloud") {
|
|
232
|
+
throw new Error("Cloud access token not provided. Cannot fetch cloud quota.");
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
// Try local
|
|
236
|
+
if (!shellRunner) {
|
|
237
|
+
throw new Error("Shell runner required for local quota fetching");
|
|
238
|
+
}
|
|
239
|
+
const localResult = await fetchAntigravityStatus(shellRunner);
|
|
240
|
+
const models = localResult.userStatus.cascadeModelConfigData?.clientModelConfigs || [];
|
|
241
|
+
const categories = groupModelsByCategory(models);
|
|
242
|
+
return {
|
|
243
|
+
source: "local",
|
|
244
|
+
categories,
|
|
245
|
+
models,
|
|
246
|
+
timestamp: localResult.timestamp,
|
|
247
|
+
};
|
|
248
|
+
}
|