@monotykamary/pi-better-grok 0.1.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/LICENSE +21 -0
- package/README.md +65 -0
- package/THIRD_PARTY_NOTICES.md +19 -0
- package/index.ts +666 -0
- package/package.json +66 -0
- package/src/config.ts +314 -0
- package/src/fast-controller.ts +82 -0
- package/src/format.ts +66 -0
- package/src/grok-auth.ts +242 -0
- package/src/identity.ts +7 -0
- package/src/paths.ts +17 -0
- package/src/usage-controller.ts +210 -0
- package/src/usage.ts +351 -0
package/src/grok-auth.ts
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { piAgentDir } from "./paths.ts";
|
|
5
|
+
|
|
6
|
+
// Native pi providers that may hold an xAI OAuth credential, in preference order.
|
|
7
|
+
// `xai` is pi-core's native provider; `xai-oauth`/`xai-auth` come from community
|
|
8
|
+
// provider extensions that share the same OAuth surface.
|
|
9
|
+
export const XAI_PROVIDER_IDS = ["xai", "xai-oauth", "xai-auth"] as const;
|
|
10
|
+
export type XaiProviderId = (typeof XAI_PROVIDER_IDS)[number];
|
|
11
|
+
|
|
12
|
+
export const GROK_CLI_AUTH_SCOPE_KEY = "https://auth.x.ai::b1a00492-073a-47ea-816f-4c329264a828";
|
|
13
|
+
export const GROK_CLI_LEGACY_AUTH_SCOPE_KEY = "https://accounts.x.ai/sign-in";
|
|
14
|
+
|
|
15
|
+
export type GrokCredential = {
|
|
16
|
+
token: string;
|
|
17
|
+
source: "modelRegistry" | "authFile" | "grokCli";
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export function isXaiProvider(provider: unknown): provider is XaiProviderId {
|
|
21
|
+
return typeof provider === "string" && (XAI_PROVIDER_IDS as readonly string[]).includes(provider);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function providerIdsFor(ctx: Pick<ExtensionContext, "model">): XaiProviderId[] {
|
|
25
|
+
if (isXaiProvider(ctx?.model?.provider)) return [ctx.model.provider];
|
|
26
|
+
return [...XAI_PROVIDER_IDS];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function readJsonFile(path: string): Record<string, unknown> | undefined {
|
|
30
|
+
try {
|
|
31
|
+
const parsed: unknown = JSON.parse(readFileSync(path, "utf8"));
|
|
32
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
33
|
+
? (parsed as Record<string, unknown>)
|
|
34
|
+
: undefined;
|
|
35
|
+
} catch {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function parseExpiry(value: unknown): number | undefined {
|
|
41
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
42
|
+
if (typeof value !== "string" || !value.trim()) return undefined;
|
|
43
|
+
const numeric = Number(value);
|
|
44
|
+
if (Number.isFinite(numeric)) return numeric;
|
|
45
|
+
const parsed = Date.parse(value);
|
|
46
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function oauthAccessFromStored(stored: unknown): string | null {
|
|
50
|
+
if (
|
|
51
|
+
stored &&
|
|
52
|
+
typeof stored === "object" &&
|
|
53
|
+
(stored as Record<string, unknown>).type === "oauth"
|
|
54
|
+
) {
|
|
55
|
+
const record = stored as { access?: unknown; expires?: unknown };
|
|
56
|
+
if (typeof record.access === "string" && record.access) {
|
|
57
|
+
const expires = parseExpiry(record.expires);
|
|
58
|
+
if (expires !== undefined && expires <= Date.now()) return null;
|
|
59
|
+
return record.access;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function extractBearerToken(authLike: unknown): string | null {
|
|
66
|
+
if (!authLike || typeof authLike !== "object") return null;
|
|
67
|
+
const payload = authLike as { apiKey?: unknown; headers?: Record<string, string> };
|
|
68
|
+
if (typeof payload.apiKey === "string" && payload.apiKey) return payload.apiKey;
|
|
69
|
+
const authorization =
|
|
70
|
+
typeof payload.headers?.Authorization === "string" ? payload.headers.Authorization : "";
|
|
71
|
+
return authorization.toLowerCase().startsWith("bearer ")
|
|
72
|
+
? authorization.slice("bearer ".length).trim() || null
|
|
73
|
+
: null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function tokenFromAuthResolution(resolution: unknown): string | null {
|
|
77
|
+
if (!resolution || typeof resolution !== "object") return null;
|
|
78
|
+
const value = resolution as {
|
|
79
|
+
ok?: unknown;
|
|
80
|
+
auth?: unknown;
|
|
81
|
+
apiKey?: unknown;
|
|
82
|
+
headers?: Record<string, string>;
|
|
83
|
+
};
|
|
84
|
+
if (value.auth && typeof value.auth === "object") return extractBearerToken(value.auth);
|
|
85
|
+
if ("ok" in value) {
|
|
86
|
+
if (value.ok !== true) return null;
|
|
87
|
+
return extractBearerToken(value);
|
|
88
|
+
}
|
|
89
|
+
return extractBearerToken(value);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function registryUsesOAuth(registry: unknown, model: unknown, providerId: string): boolean {
|
|
93
|
+
try {
|
|
94
|
+
const reg = registry as {
|
|
95
|
+
isUsingOAuth?: (model: unknown) => boolean;
|
|
96
|
+
authStorage?: { get?: (id: string) => unknown };
|
|
97
|
+
};
|
|
98
|
+
if (typeof reg?.isUsingOAuth === "function") return reg.isUsingOAuth(model) === true;
|
|
99
|
+
const stored = reg?.authStorage?.get?.(providerId) as
|
|
100
|
+
| { type?: unknown; access?: unknown }
|
|
101
|
+
| undefined;
|
|
102
|
+
return stored?.type === "oauth" && typeof stored.access === "string" && !!stored.access;
|
|
103
|
+
} catch {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function resolveRegistryToken(
|
|
109
|
+
registry: unknown,
|
|
110
|
+
model: unknown,
|
|
111
|
+
modelRuntime: unknown,
|
|
112
|
+
): Promise<string | null> {
|
|
113
|
+
const reg = registry as {
|
|
114
|
+
getAuth?: (model: unknown) => Promise<unknown>;
|
|
115
|
+
getApiKeyAndHeaders?: (model: unknown) => Promise<unknown>;
|
|
116
|
+
getProviderAuth?: (providerId: string) => Promise<unknown>;
|
|
117
|
+
};
|
|
118
|
+
const runtime = modelRuntime as { getAuth?: (model: unknown) => Promise<unknown> } | undefined;
|
|
119
|
+
if (runtime && typeof runtime.getAuth === "function") {
|
|
120
|
+
try {
|
|
121
|
+
const token = tokenFromAuthResolution(await runtime.getAuth(model));
|
|
122
|
+
if (token) return token;
|
|
123
|
+
} catch {
|
|
124
|
+
// Fall through to registry projections.
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (reg && typeof reg.getAuth === "function") {
|
|
128
|
+
try {
|
|
129
|
+
const token = tokenFromAuthResolution(await reg.getAuth(model));
|
|
130
|
+
if (token) return token;
|
|
131
|
+
} catch {
|
|
132
|
+
// Fall through.
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (typeof reg?.getApiKeyAndHeaders === "function") {
|
|
136
|
+
try {
|
|
137
|
+
const token = tokenFromAuthResolution(await reg.getApiKeyAndHeaders(model));
|
|
138
|
+
if (token) return token;
|
|
139
|
+
} catch {
|
|
140
|
+
// Fall through.
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (typeof reg?.getProviderAuth === "function") {
|
|
144
|
+
try {
|
|
145
|
+
const providerId =
|
|
146
|
+
typeof (model as { provider?: unknown })?.provider === "string"
|
|
147
|
+
? (model as { provider: string }).provider
|
|
148
|
+
: "xai";
|
|
149
|
+
return tokenFromAuthResolution(await reg.getProviderAuth(providerId));
|
|
150
|
+
} catch {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function piAuthPath(env: NodeJS.ProcessEnv = process.env): string {
|
|
158
|
+
return piAgentDir(env) + "/auth.json";
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function grokCliAuthPath(env: NodeJS.ProcessEnv = process.env, home = homedir()): string {
|
|
162
|
+
const override = env.PI_GROK_AUTH_PATH?.trim();
|
|
163
|
+
if (override) return override;
|
|
164
|
+
return home + "/.grok/auth.json";
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function readPiStoredOAuthToken(env: NodeJS.ProcessEnv = process.env): string | null {
|
|
168
|
+
const data = readJsonFile(piAuthPath(env));
|
|
169
|
+
if (!data) return null;
|
|
170
|
+
for (const providerId of XAI_PROVIDER_IDS) {
|
|
171
|
+
const token = oauthAccessFromStored(data[providerId]);
|
|
172
|
+
if (token) return token;
|
|
173
|
+
}
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function readGrokCliToken(env: NodeJS.ProcessEnv = process.env): string | null {
|
|
178
|
+
const data = readJsonFile(grokCliAuthPath(env));
|
|
179
|
+
if (!data) return null;
|
|
180
|
+
for (const scopeKey of [GROK_CLI_AUTH_SCOPE_KEY, GROK_CLI_LEGACY_AUTH_SCOPE_KEY]) {
|
|
181
|
+
const entry = data[scopeKey] as
|
|
182
|
+
| { key?: unknown; access_token?: unknown; token?: unknown; expires_at?: unknown }
|
|
183
|
+
| undefined;
|
|
184
|
+
if (entry && typeof entry === "object") {
|
|
185
|
+
const access = [entry.key, entry.access_token, entry.token].find(
|
|
186
|
+
(value): value is string => typeof value === "string" && value.length > 0,
|
|
187
|
+
);
|
|
188
|
+
if (access) {
|
|
189
|
+
const expires = parseExpiry(entry.expires_at);
|
|
190
|
+
if (expires !== undefined && expires <= Date.now()) return null;
|
|
191
|
+
return access;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
const top = data.access_token ?? data.token;
|
|
196
|
+
return typeof top === "string" && top ? top : null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export async function resolveGrokCredential(
|
|
200
|
+
ctx: Pick<ExtensionContext, "model" | "modelRegistry">,
|
|
201
|
+
): Promise<GrokCredential | null> {
|
|
202
|
+
const registry = (ctx as { modelRegistry?: unknown })?.modelRegistry;
|
|
203
|
+
const modelRuntime = (ctx as { modelRuntime?: unknown })?.modelRuntime;
|
|
204
|
+
if (registry && typeof registry === "object" && "find" in registry) {
|
|
205
|
+
const reg = registry as { find: (provider: string, id: string) => unknown };
|
|
206
|
+
for (const providerId of providerIdsFor(ctx)) {
|
|
207
|
+
const candidates = [
|
|
208
|
+
ctx?.model?.provider === providerId ? ctx.model : undefined,
|
|
209
|
+
reg.find(
|
|
210
|
+
providerId,
|
|
211
|
+
ctx?.model?.provider === providerId ? (ctx.model?.id ?? "") : "grok-4.5",
|
|
212
|
+
),
|
|
213
|
+
reg.find(providerId, "grok-4.5"),
|
|
214
|
+
].filter(Boolean);
|
|
215
|
+
for (const model of candidates) {
|
|
216
|
+
if (!registryUsesOAuth(registry, model, providerId)) continue;
|
|
217
|
+
const token = await resolveRegistryToken(registry, model, modelRuntime);
|
|
218
|
+
if (token) return { token, source: "modelRegistry" };
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
const stored = readPiStoredOAuthToken();
|
|
223
|
+
if (stored) return { token: stored, source: "authFile" };
|
|
224
|
+
const grok = readGrokCliToken();
|
|
225
|
+
if (grok) return { token: grok, source: "grokCli" };
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function hasGrokOAuth(ctx: Pick<ExtensionContext, "model" | "modelRegistry">): boolean {
|
|
230
|
+
const registry = (ctx as { modelRegistry?: unknown })?.modelRegistry;
|
|
231
|
+
if (registry && typeof registry === "object" && "find" in registry) {
|
|
232
|
+
for (const providerId of providerIdsFor(ctx)) {
|
|
233
|
+
const reg = registry as { find: (provider: string, id: string) => unknown };
|
|
234
|
+
const candidates = [
|
|
235
|
+
ctx?.model?.provider === providerId ? ctx.model : undefined,
|
|
236
|
+
reg.find(providerId, "grok-4.5"),
|
|
237
|
+
].filter(Boolean);
|
|
238
|
+
if (candidates.some((model) => registryUsesOAuth(registry, model, providerId))) return true;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return !!(readPiStoredOAuthToken() || readGrokCliToken());
|
|
242
|
+
}
|
package/src/identity.ts
ADDED
package/src/paths.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
export function expandTildePath(path: string, home = homedir()): string {
|
|
5
|
+
if (path === "~") return home;
|
|
6
|
+
if (path.startsWith("~/") || path.startsWith("~\\")) return join(home, path.slice(2));
|
|
7
|
+
return path;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function piAgentDir(env: NodeJS.ProcessEnv = process.env, home = homedir()): string {
|
|
11
|
+
const configuredDir = env.PI_CODING_AGENT_DIR?.trim();
|
|
12
|
+
return configuredDir ? expandTildePath(configuredDir, home) : join(home, ".pi", "agent");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function resolveUserPath(path: string, cwd: string, home = homedir()): string {
|
|
16
|
+
return resolve(cwd, expandTildePath(path, home));
|
|
17
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { ResolvedConfig } from "./config.ts";
|
|
3
|
+
import { isXaiProvider, readPiStoredOAuthToken } from "./grok-auth.ts";
|
|
4
|
+
import { sanitizeDiagnosticError } from "./format.ts";
|
|
5
|
+
import { currentModelKey } from "./fast-controller.ts";
|
|
6
|
+
import { formatUsageDetail, formatUsageSnapshot, type UsageSnapshot } from "./usage.ts";
|
|
7
|
+
|
|
8
|
+
export function isGrokSubscriptionModel(
|
|
9
|
+
ctx: Pick<ExtensionContext, "model" | "modelRegistry">,
|
|
10
|
+
cfg: ResolvedConfig,
|
|
11
|
+
isUsingOAuth?: boolean,
|
|
12
|
+
): boolean {
|
|
13
|
+
const model = ctx.model;
|
|
14
|
+
if (!model) return false;
|
|
15
|
+
const provider = String(model.provider ?? "");
|
|
16
|
+
const idLooksGrok = String(model.id ?? "")
|
|
17
|
+
.toLowerCase()
|
|
18
|
+
.startsWith("grok-");
|
|
19
|
+
if (!isXaiProvider(provider) && provider !== "grok-build" && !idLooksGrok) return false;
|
|
20
|
+
if (!cfg.usage.showOnlyOnSubscriptionModels) return true;
|
|
21
|
+
if (isUsingOAuth !== undefined) return isUsingOAuth;
|
|
22
|
+
const registry = (ctx as { modelRegistry?: { isUsingOAuth?: (model: unknown) => boolean } })
|
|
23
|
+
.modelRegistry;
|
|
24
|
+
if (registry && typeof registry.isUsingOAuth === "function") {
|
|
25
|
+
try {
|
|
26
|
+
return registry.isUsingOAuth(model) === true;
|
|
27
|
+
} catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const STALE_EXTENSION_CONTEXT_MESSAGE = "This extension ctx is stale";
|
|
35
|
+
|
|
36
|
+
function isStaleExtensionContextError(error: unknown): boolean {
|
|
37
|
+
return error instanceof Error && error.message.includes(STALE_EXTENSION_CONTEXT_MESSAGE);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
type UsageRefreshOptions = { notify?: boolean; force?: boolean };
|
|
41
|
+
|
|
42
|
+
type QueuedUsageRefresh = {
|
|
43
|
+
ctx: ExtensionContext;
|
|
44
|
+
notify: boolean;
|
|
45
|
+
force: boolean;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export type FetchUsageSnapshot = (ctx: ExtensionContext) => Promise<UsageSnapshot>;
|
|
49
|
+
|
|
50
|
+
export class UsageController {
|
|
51
|
+
private usageSnapshot: UsageSnapshot | undefined;
|
|
52
|
+
private usageUpdatedAt: number | undefined;
|
|
53
|
+
private usageError: string | undefined;
|
|
54
|
+
private usageLastFetchAt: number | undefined;
|
|
55
|
+
private usageTimer: ReturnType<typeof setInterval> | undefined;
|
|
56
|
+
private usageRefreshInFlight = false;
|
|
57
|
+
private queuedUsageRefresh: QueuedUsageRefresh | undefined;
|
|
58
|
+
private shuttingDown = false;
|
|
59
|
+
private readonly getConfig: (ctx: ExtensionContext) => ResolvedConfig;
|
|
60
|
+
private readonly updateFooter: (ctx: ExtensionContext) => void;
|
|
61
|
+
private readonly fetchSnapshot: FetchUsageSnapshot;
|
|
62
|
+
|
|
63
|
+
constructor(
|
|
64
|
+
getConfig: (ctx: ExtensionContext) => ResolvedConfig,
|
|
65
|
+
updateFooter: (ctx: ExtensionContext) => void,
|
|
66
|
+
fetchSnapshot: FetchUsageSnapshot,
|
|
67
|
+
) {
|
|
68
|
+
this.getConfig = getConfig;
|
|
69
|
+
this.updateFooter = updateFooter;
|
|
70
|
+
this.fetchSnapshot = fetchSnapshot;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
get snapshot(): UsageSnapshot | undefined {
|
|
74
|
+
return this.usageSnapshot;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
statusLine(
|
|
78
|
+
ctx: ExtensionContext,
|
|
79
|
+
cfg = this.getConfig(ctx),
|
|
80
|
+
isUsingOAuth?: boolean,
|
|
81
|
+
): string | undefined {
|
|
82
|
+
return this.usageSnapshot &&
|
|
83
|
+
!this.usageError &&
|
|
84
|
+
cfg.usage.enabled &&
|
|
85
|
+
isGrokSubscriptionModel(ctx, cfg, isUsingOAuth)
|
|
86
|
+
? formatUsageSnapshot(this.usageSnapshot, cfg.usage)
|
|
87
|
+
: undefined;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
formatStatus(ctx: ExtensionContext): string {
|
|
91
|
+
const cfg = this.getConfig(ctx);
|
|
92
|
+
if (!cfg.usage.enabled) return "Usage display is disabled.";
|
|
93
|
+
if (!isGrokSubscriptionModel(ctx, cfg)) {
|
|
94
|
+
return "Usage hidden: current model is not an xAI subscription model.";
|
|
95
|
+
}
|
|
96
|
+
if (this.usageError) return `Usage unavailable: ${this.usageError}`;
|
|
97
|
+
if (!this.usageSnapshot) return "Usage unavailable.";
|
|
98
|
+
const stale =
|
|
99
|
+
this.usageUpdatedAt && Date.now() - this.usageUpdatedAt > cfg.usage.refreshIntervalMs * 2
|
|
100
|
+
? ` · stale`
|
|
101
|
+
: "";
|
|
102
|
+
return `${formatUsageSnapshot(this.usageSnapshot, cfg.usage)}${stale}`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
formatDetail(ctx: ExtensionContext): string {
|
|
106
|
+
if (!this.usageSnapshot) return this.formatStatus(ctx);
|
|
107
|
+
return formatUsageDetail(this.usageSnapshot);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
formatDebug(ctx: ExtensionContext): string {
|
|
111
|
+
const cfg = this.getConfig(ctx);
|
|
112
|
+
const credential = readPiStoredOAuthToken() ? "auth file" : undefined;
|
|
113
|
+
return [
|
|
114
|
+
`Usage enabled: ${cfg.usage.enabled}`,
|
|
115
|
+
`Current model: ${currentModelKey(ctx) ?? "none"}`,
|
|
116
|
+
`Current model eligible: ${isGrokSubscriptionModel(ctx, cfg)}`,
|
|
117
|
+
`Requires subscription model: ${cfg.usage.showOnlyOnSubscriptionModels}`,
|
|
118
|
+
`Auth: ${credential ?? resolveSourceHint()}`,
|
|
119
|
+
`Last fetch: ${this.usageLastFetchAt ? new Date(this.usageLastFetchAt).toLocaleTimeString() : "never"}`,
|
|
120
|
+
`Last successful update: ${this.usageUpdatedAt ? new Date(this.usageUpdatedAt).toLocaleTimeString() : "never"}`,
|
|
121
|
+
`Last error: ${this.usageError ?? "none"}`,
|
|
122
|
+
`Config: ${cfg.configPath}`,
|
|
123
|
+
].join("\n");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
start(ctx: ExtensionContext): void {
|
|
127
|
+
this.stopTimer();
|
|
128
|
+
this.shuttingDown = false;
|
|
129
|
+
const intervalMs = this.getConfig(ctx).usage.refreshIntervalMs;
|
|
130
|
+
this.usageTimer = setInterval(() => {
|
|
131
|
+
void this.refresh(ctx).catch(() => undefined);
|
|
132
|
+
}, intervalMs);
|
|
133
|
+
this.usageTimer.unref?.();
|
|
134
|
+
void this.refresh(ctx).catch(() => undefined);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
restart(ctx: ExtensionContext): void {
|
|
138
|
+
this.start(ctx);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async refresh(ctx: ExtensionContext, options: UsageRefreshOptions = {}): Promise<void> {
|
|
142
|
+
if (this.shuttingDown) return;
|
|
143
|
+
if (this.usageRefreshInFlight) {
|
|
144
|
+
this.queuedUsageRefresh = {
|
|
145
|
+
ctx,
|
|
146
|
+
notify: (this.queuedUsageRefresh?.notify ?? false) || options.notify === true,
|
|
147
|
+
force: (this.queuedUsageRefresh?.force ?? false) || options.force === true,
|
|
148
|
+
};
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
this.usageRefreshInFlight = true;
|
|
152
|
+
try {
|
|
153
|
+
await this.doRefresh(ctx, options);
|
|
154
|
+
} finally {
|
|
155
|
+
this.usageRefreshInFlight = false;
|
|
156
|
+
const queued = this.queuedUsageRefresh;
|
|
157
|
+
this.queuedUsageRefresh = undefined;
|
|
158
|
+
if (queued && !this.shuttingDown) {
|
|
159
|
+
await this.refresh(queued.ctx, { notify: queued.notify, force: queued.force }).catch(
|
|
160
|
+
() => undefined,
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
private async doRefresh(ctx: ExtensionContext, options: UsageRefreshOptions): Promise<void> {
|
|
167
|
+
const cfg = this.getConfig(ctx);
|
|
168
|
+
if (!isGrokSubscriptionModel(ctx, cfg)) {
|
|
169
|
+
if (options.notify) {
|
|
170
|
+
ctx.ui.notify("Usage hidden: current model is not an xAI subscription model.", "info");
|
|
171
|
+
}
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
try {
|
|
175
|
+
const snapshot = await this.fetchSnapshot(ctx);
|
|
176
|
+
this.usageSnapshot = snapshot;
|
|
177
|
+
this.usageUpdatedAt = Date.now();
|
|
178
|
+
this.usageLastFetchAt = this.usageUpdatedAt;
|
|
179
|
+
this.usageError = undefined;
|
|
180
|
+
this.updateFooter(ctx);
|
|
181
|
+
if (options.notify) ctx.ui.notify(this.formatStatus(ctx), "info");
|
|
182
|
+
} catch (error) {
|
|
183
|
+
if (isStaleExtensionContextError(error)) return;
|
|
184
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
185
|
+
this.usageError = sanitizeDiagnosticError(message);
|
|
186
|
+
this.usageLastFetchAt = Date.now();
|
|
187
|
+
this.updateFooter(ctx);
|
|
188
|
+
if (options.notify) {
|
|
189
|
+
ctx.ui.notify(`Grok usage unavailable: ${this.usageError}`, "warning");
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
shutdown(): void {
|
|
195
|
+
this.shuttingDown = true;
|
|
196
|
+
this.stopTimer();
|
|
197
|
+
this.queuedUsageRefresh = undefined;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private stopTimer(): void {
|
|
201
|
+
if (this.usageTimer !== undefined) {
|
|
202
|
+
clearInterval(this.usageTimer);
|
|
203
|
+
this.usageTimer = undefined;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function resolveSourceHint(): string {
|
|
209
|
+
return "missing (run /login xai)";
|
|
210
|
+
}
|