tau-coding-agent 0.1.3 → 0.1.4
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/extensions/answer.ts +4 -4
- package/extensions/branch-term.ts +3 -3
- package/extensions/btw.ts +4 -4
- package/extensions/ghostty.ts +1 -1
- package/extensions/git-diff-stats.ts +1 -1
- package/extensions/git-pr-status.ts +1 -1
- package/extensions/insights.ts +3 -3
- package/extensions/interlude.ts +1 -1
- package/extensions/loop.ts +3 -3
- package/extensions/memory.ts +8 -2
- package/extensions/notify.ts +1 -1
- package/extensions/openai-fast.ts +1 -1
- package/extensions/openai-verbosity.ts +1 -1
- package/extensions/review.ts +111 -38
- package/extensions/sandbox/index.ts +35 -7
- package/extensions/tool-display-mode.ts +171 -14
- package/extensions/usage/anthropic.ts +1 -1
- package/extensions/usage/github-copilot.ts +1 -1
- package/extensions/usage/index.ts +3 -3
- package/extensions/usage/minimax.ts +1 -1
- package/extensions/usage/openai-codex.ts +1 -1
- package/extensions/usage/openrouter.ts +1 -1
- package/extensions/usage/providers.ts +0 -2
- package/extensions/usage/types.ts +1 -2
- package/extensions/usage/zai.ts +1 -1
- package/extensions/websearch/index.ts +3 -3
- package/extensions/websearch/providers/anthropic.pi.ts +1 -1
- package/extensions/websearch/providers/gemini.pi.ts +1 -1
- package/extensions/websearch/providers/openai-codex.pi.ts +1 -1
- package/extensions/websearch/providers/pi-model.shared.ts +2 -2
- package/extensions/worktree.ts +3 -3
- package/package.json +5 -5
- package/skills/oracle/scripts/oracle +0 -1
- package/extensions/usage/google-gemini-cli.ts +0 -232
|
@@ -2,6 +2,7 @@ import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import {
|
|
5
|
+
type AppKeybinding,
|
|
5
6
|
CustomEditor,
|
|
6
7
|
createBashToolDefinition,
|
|
7
8
|
createFindToolDefinition,
|
|
@@ -16,8 +17,15 @@ import {
|
|
|
16
17
|
type ToolDefinition,
|
|
17
18
|
type ToolInfo,
|
|
18
19
|
type ToolRenderResultOptions,
|
|
19
|
-
} from "@
|
|
20
|
-
import {
|
|
20
|
+
} from "@earendil-works/pi-coding-agent";
|
|
21
|
+
import {
|
|
22
|
+
Container,
|
|
23
|
+
Text,
|
|
24
|
+
type AutocompleteProvider,
|
|
25
|
+
type Component,
|
|
26
|
+
type EditorComponent,
|
|
27
|
+
type Focusable,
|
|
28
|
+
} from "@earendil-works/pi-tui";
|
|
21
29
|
|
|
22
30
|
// --- Constants ---
|
|
23
31
|
|
|
@@ -46,11 +54,22 @@ type ToolName = keyof typeof TOOL_FACTORIES;
|
|
|
46
54
|
type AnyToolDefinition = ToolDefinition<any, any, any>;
|
|
47
55
|
type AnyToolRenderContext = Parameters<NonNullable<AnyToolDefinition["renderResult"]>>[3];
|
|
48
56
|
type JsonObject = Record<string, unknown>;
|
|
57
|
+
type EditorFactory = NonNullable<ReturnType<ExtensionContext["ui"]["getEditorComponent"]>>;
|
|
49
58
|
|
|
50
59
|
type ToolDisplayModeConfig = {
|
|
51
60
|
mode: Mode;
|
|
52
61
|
};
|
|
53
62
|
|
|
63
|
+
type CustomEditorLike = EditorComponent &
|
|
64
|
+
Partial<Focusable> & {
|
|
65
|
+
actionHandlers?: Map<AppKeybinding, () => void>;
|
|
66
|
+
onEscape?: () => void;
|
|
67
|
+
onCtrlD?: () => void;
|
|
68
|
+
onPasteImage?: () => void;
|
|
69
|
+
onExtensionShortcut?: (data: string) => boolean;
|
|
70
|
+
isShowingAutocomplete?: () => boolean;
|
|
71
|
+
};
|
|
72
|
+
|
|
54
73
|
// --- Config ---
|
|
55
74
|
|
|
56
75
|
function emptyConfig(): ToolDisplayModeConfig {
|
|
@@ -242,11 +261,11 @@ function renderMinimalResult(
|
|
|
242
261
|
break;
|
|
243
262
|
}
|
|
244
263
|
|
|
245
|
-
return renderMinimalText(theme, "muted",
|
|
264
|
+
return renderMinimalText(theme, "muted", `↳ ${summary}`);
|
|
246
265
|
}
|
|
247
266
|
|
|
248
267
|
function renderMinimalText(theme: Theme, color: "error" | "muted", text: string): Text {
|
|
249
|
-
return new Text(theme.fg(color,
|
|
268
|
+
return new Text(theme.fg(color, text), 0, 0);
|
|
250
269
|
}
|
|
251
270
|
|
|
252
271
|
function bashSummary(result: AgentToolResult<any>): string {
|
|
@@ -328,29 +347,152 @@ function emptyComponent(): Container {
|
|
|
328
347
|
|
|
329
348
|
// --- Editor ---
|
|
330
349
|
|
|
331
|
-
class ToolDisplayEditor
|
|
350
|
+
class ToolDisplayEditor implements EditorComponent, Focusable {
|
|
351
|
+
readonly actionHandlers: Map<AppKeybinding, () => void>;
|
|
352
|
+
private readonly customBase: CustomEditorLike;
|
|
353
|
+
private fallbackFocused = false;
|
|
354
|
+
private fallbackOnEscape?: () => void;
|
|
355
|
+
private fallbackOnCtrlD?: () => void;
|
|
356
|
+
private fallbackOnPasteImage?: () => void;
|
|
357
|
+
private fallbackOnExtensionShortcut?: (data: string) => boolean;
|
|
358
|
+
|
|
332
359
|
constructor(
|
|
333
|
-
|
|
334
|
-
theme: EditorTheme,
|
|
360
|
+
private readonly base: EditorComponent,
|
|
335
361
|
private readonly appKeybindings: KeybindingsManager,
|
|
336
362
|
private readonly cycleMode: () => void,
|
|
337
363
|
) {
|
|
338
|
-
|
|
364
|
+
this.customBase = base as CustomEditorLike;
|
|
365
|
+
this.actionHandlers = this.customBase.actionHandlers ?? new Map();
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
get focused(): boolean {
|
|
369
|
+
return this.customBase.focused ?? this.fallbackFocused;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
set focused(value: boolean) {
|
|
373
|
+
this.fallbackFocused = value;
|
|
374
|
+
if ("focused" in this.customBase) this.customBase.focused = value;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
get onSubmit(): ((text: string) => void) | undefined {
|
|
378
|
+
return this.base.onSubmit;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
set onSubmit(handler: ((text: string) => void) | undefined) {
|
|
382
|
+
this.base.onSubmit = handler;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
get onChange(): ((text: string) => void) | undefined {
|
|
386
|
+
return this.base.onChange;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
set onChange(handler: ((text: string) => void) | undefined) {
|
|
390
|
+
this.base.onChange = handler;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
get wantsKeyRelease(): boolean | undefined {
|
|
394
|
+
return this.base.wantsKeyRelease;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
get borderColor(): ((str: string) => string) | undefined {
|
|
398
|
+
return this.base.borderColor;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
set borderColor(handler: ((str: string) => string) | undefined) {
|
|
402
|
+
this.base.borderColor = handler;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
get onEscape(): (() => void) | undefined {
|
|
406
|
+
return this.customBase.onEscape ?? this.fallbackOnEscape;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
set onEscape(handler: (() => void) | undefined) {
|
|
410
|
+
this.fallbackOnEscape = handler;
|
|
411
|
+
if ("onEscape" in this.customBase) this.customBase.onEscape = handler;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
get onCtrlD(): (() => void) | undefined {
|
|
415
|
+
return this.customBase.onCtrlD ?? this.fallbackOnCtrlD;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
set onCtrlD(handler: (() => void) | undefined) {
|
|
419
|
+
this.fallbackOnCtrlD = handler;
|
|
420
|
+
if ("onCtrlD" in this.customBase) this.customBase.onCtrlD = handler;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
get onPasteImage(): (() => void) | undefined {
|
|
424
|
+
return this.customBase.onPasteImage ?? this.fallbackOnPasteImage;
|
|
339
425
|
}
|
|
340
426
|
|
|
341
|
-
|
|
427
|
+
set onPasteImage(handler: (() => void) | undefined) {
|
|
428
|
+
this.fallbackOnPasteImage = handler;
|
|
429
|
+
if ("onPasteImage" in this.customBase) this.customBase.onPasteImage = handler;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
get onExtensionShortcut(): ((data: string) => boolean) | undefined {
|
|
433
|
+
return this.customBase.onExtensionShortcut ?? this.fallbackOnExtensionShortcut;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
set onExtensionShortcut(handler: ((data: string) => boolean) | undefined) {
|
|
437
|
+
this.fallbackOnExtensionShortcut = handler;
|
|
438
|
+
if ("onExtensionShortcut" in this.customBase) this.customBase.onExtensionShortcut = handler;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
getText(): string {
|
|
442
|
+
return this.base.getText();
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
setText(text: string): void {
|
|
446
|
+
this.base.setText(text);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
addToHistory(text: string): void {
|
|
450
|
+
this.base.addToHistory?.(text);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
insertTextAtCursor(text: string): void {
|
|
454
|
+
this.base.insertTextAtCursor?.(text);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
getExpandedText(): string {
|
|
458
|
+
return this.base.getExpandedText?.() ?? this.base.getText();
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
setAutocompleteProvider(provider: AutocompleteProvider): void {
|
|
462
|
+
this.base.setAutocompleteProvider?.(provider);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
setPaddingX(padding: number): void {
|
|
466
|
+
this.base.setPaddingX?.(padding);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
setAutocompleteMaxVisible(maxVisible: number): void {
|
|
470
|
+
this.base.setAutocompleteMaxVisible?.(maxVisible);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
render(width: number): string[] {
|
|
474
|
+
return this.base.render(width);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
invalidate(): void {
|
|
478
|
+
this.base.invalidate();
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
handleInput(data: string): void {
|
|
342
482
|
if (this.appKeybindings.matches(data, "app.tools.expand")) {
|
|
343
483
|
this.cycleMode();
|
|
344
484
|
return;
|
|
345
485
|
}
|
|
346
486
|
|
|
347
|
-
|
|
487
|
+
this.base.handleInput(data);
|
|
348
488
|
}
|
|
349
489
|
}
|
|
350
490
|
|
|
351
491
|
export default function toolDisplayModeExtension(pi: ExtensionAPI): void {
|
|
352
492
|
let mode: Mode = DEFAULT_MODE;
|
|
353
493
|
let registeredToolRenderers = false;
|
|
494
|
+
let installedEditorFactory: EditorFactory | undefined;
|
|
495
|
+
let previousEditorFactory: EditorFactory | undefined;
|
|
354
496
|
const toolCache = new Map<string, Partial<Record<ToolName, AnyToolDefinition>>>();
|
|
355
497
|
|
|
356
498
|
const setMode = (ctx: ExtensionContext, next: Mode): void => {
|
|
@@ -379,9 +521,24 @@ export default function toolDisplayModeExtension(pi: ExtensionAPI): void {
|
|
|
379
521
|
applyMode(ctx, mode);
|
|
380
522
|
if (!ctx.hasUI) return;
|
|
381
523
|
|
|
382
|
-
ctx.ui.
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
)
|
|
524
|
+
if (ctx.ui.getEditorComponent() === installedEditorFactory) return;
|
|
525
|
+
|
|
526
|
+
previousEditorFactory = ctx.ui.getEditorComponent();
|
|
527
|
+
installedEditorFactory = (tui, theme, keybindings) => {
|
|
528
|
+
const baseEditor =
|
|
529
|
+
previousEditorFactory?.(tui, theme, keybindings) ??
|
|
530
|
+
new CustomEditor(tui, theme, keybindings);
|
|
531
|
+
return new ToolDisplayEditor(baseEditor, keybindings, () => setMode(ctx, nextMode(mode)));
|
|
532
|
+
};
|
|
533
|
+
ctx.ui.setEditorComponent(installedEditorFactory);
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
pi.on("session_shutdown", async (_event, ctx) => {
|
|
537
|
+
if (ctx.hasUI && ctx.ui.getEditorComponent() === installedEditorFactory) {
|
|
538
|
+
ctx.ui.setEditorComponent(previousEditorFactory);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
installedEditorFactory = undefined;
|
|
542
|
+
previousEditorFactory = undefined;
|
|
386
543
|
});
|
|
387
544
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { ExtensionAPI, ExtensionContext } from "@
|
|
2
|
-
import { BorderedLoader } from "@
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { BorderedLoader } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import {
|
|
4
4
|
Key,
|
|
5
5
|
matchesKey,
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
type TUI,
|
|
8
8
|
truncateToWidth,
|
|
9
9
|
visibleWidth,
|
|
10
|
-
} from "@
|
|
10
|
+
} from "@earendil-works/pi-tui";
|
|
11
11
|
import { createReadStream, type Dirent } from "node:fs";
|
|
12
12
|
import fs from "node:fs/promises";
|
|
13
13
|
import os from "node:os";
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import anthropicProvider from "./anthropic.js";
|
|
2
2
|
import githubCopilotProvider from "./github-copilot.js";
|
|
3
|
-
import googleGeminiCliProvider from "./google-gemini-cli.js";
|
|
4
3
|
import minimaxProvider from "./minimax.js";
|
|
5
4
|
import openAICodexProvider from "./openai-codex.js";
|
|
6
5
|
import openRouterProvider from "./openrouter.js";
|
|
@@ -11,7 +10,6 @@ export const SUPPORTED_PROVIDERS: readonly UsageProviderDefinition[] = [
|
|
|
11
10
|
openAICodexProvider,
|
|
12
11
|
anthropicProvider,
|
|
13
12
|
githubCopilotProvider,
|
|
14
|
-
googleGeminiCliProvider,
|
|
15
13
|
openRouterProvider,
|
|
16
14
|
zaiProvider,
|
|
17
15
|
minimaxProvider,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ExtensionContext } from "@
|
|
1
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
|
|
3
3
|
export interface RGB {
|
|
4
4
|
r: number;
|
|
@@ -10,7 +10,6 @@ export type SupportedProviderId =
|
|
|
10
10
|
| "openai-codex"
|
|
11
11
|
| "anthropic"
|
|
12
12
|
| "github-copilot"
|
|
13
|
-
| "google-gemini-cli"
|
|
14
13
|
| "openrouter"
|
|
15
14
|
| "zai"
|
|
16
15
|
| "minimax";
|
package/extensions/usage/zai.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import type { Api, Model } from "@
|
|
1
|
+
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
2
2
|
import {
|
|
3
3
|
defineTool,
|
|
4
4
|
getMarkdownTheme,
|
|
5
5
|
keyHint,
|
|
6
6
|
type ExtensionAPI,
|
|
7
7
|
type ExtensionContext,
|
|
8
|
-
} from "@
|
|
9
|
-
import { Container, Markdown, Spacer, Text } from "@
|
|
8
|
+
} from "@earendil-works/pi-coding-agent";
|
|
9
|
+
import { Container, Markdown, Spacer, Text } from "@earendil-works/pi-tui";
|
|
10
10
|
import { Type } from "typebox";
|
|
11
11
|
|
|
12
12
|
import { createBrowserSession, discoverProfiles } from "./browser/discovery.js";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Api, Model } from "@
|
|
1
|
+
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
2
2
|
|
|
3
3
|
import { dedupeSources, extractMarkdownSources, normalizeSourceTitle } from "../normalize.js";
|
|
4
4
|
import type { WebsearchResult, WebsearchSource } from "../types.js";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { Api, Model } from "@
|
|
2
|
-
import type { ExtensionContext } from "@
|
|
1
|
+
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
2
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
|
|
4
4
|
export interface PiModelSelection {
|
|
5
5
|
model: Model<Api>;
|
package/extensions/worktree.ts
CHANGED
|
@@ -18,14 +18,14 @@
|
|
|
18
18
|
* Lists worktrees and their status.
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
-
import type { ExtensionAPI, ExtensionCommandContext, Theme } from "@
|
|
21
|
+
import type { ExtensionAPI, ExtensionCommandContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
22
22
|
import {
|
|
23
23
|
CURRENT_SESSION_VERSION,
|
|
24
24
|
DynamicBorder,
|
|
25
25
|
SessionManager,
|
|
26
26
|
highlightCode,
|
|
27
27
|
type SessionHeader,
|
|
28
|
-
} from "@
|
|
28
|
+
} from "@earendil-works/pi-coding-agent";
|
|
29
29
|
import {
|
|
30
30
|
Box,
|
|
31
31
|
Container,
|
|
@@ -33,7 +33,7 @@ import {
|
|
|
33
33
|
SelectList,
|
|
34
34
|
Text,
|
|
35
35
|
matchesKey,
|
|
36
|
-
} from "@
|
|
36
|
+
} from "@earendil-works/pi-tui";
|
|
37
37
|
import { spawnSync } from "node:child_process";
|
|
38
38
|
import { randomUUID } from "node:crypto";
|
|
39
39
|
import * as fs from "node:fs";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tau-coding-agent",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Opinionated Pi package with web search, review workflows, sandboxing, memory, usage reporting, skills, and a theme",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent-tools",
|
|
@@ -35,10 +35,10 @@
|
|
|
35
35
|
"turndown-plugin-gfm": "^1.0.2"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
38
|
-
"@
|
|
39
|
-
"@
|
|
40
|
-
"@
|
|
41
|
-
"@
|
|
38
|
+
"@earendil-works/pi-agent-core": "*",
|
|
39
|
+
"@earendil-works/pi-ai": "*",
|
|
40
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
41
|
+
"@earendil-works/pi-tui": "*",
|
|
42
42
|
"typebox": "*"
|
|
43
43
|
},
|
|
44
44
|
"engines": {
|
|
@@ -161,7 +161,6 @@ function providerScore(provider, family) {
|
|
|
161
161
|
}
|
|
162
162
|
|
|
163
163
|
if (family === "google") {
|
|
164
|
-
if (normalized === "google-gemini-cli") return 700;
|
|
165
164
|
if (normalized === "google") return 600;
|
|
166
165
|
if (normalized === "github-copilot") return 500;
|
|
167
166
|
if (normalized.includes("vertex")) return 450;
|
|
@@ -1,232 +0,0 @@
|
|
|
1
|
-
import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
2
|
-
import {
|
|
3
|
-
clampPercent,
|
|
4
|
-
LiveUsageUnavailableError,
|
|
5
|
-
makeMeterItem,
|
|
6
|
-
parseISODate,
|
|
7
|
-
readArray,
|
|
8
|
-
readString,
|
|
9
|
-
} from "./shared.js";
|
|
10
|
-
import type { LiveUsageItem, LiveUsageSnapshot, UsageProviderDefinition } from "./types.js";
|
|
11
|
-
|
|
12
|
-
const PROVIDER_ID = "google-gemini-cli";
|
|
13
|
-
const QUOTA_URL = "https://cloudcode-pa.googleapis.com/v1internal:retrieveUserQuota";
|
|
14
|
-
const LOAD_CODE_ASSIST_URL = "https://cloudcode-pa.googleapis.com/v1internal:loadCodeAssist";
|
|
15
|
-
|
|
16
|
-
type GeminiTier = "free-tier" | "legacy-tier" | "standard-tier";
|
|
17
|
-
|
|
18
|
-
interface GeminiAuthPayload {
|
|
19
|
-
token?: string;
|
|
20
|
-
projectId?: string;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
interface GeminiQuota {
|
|
24
|
-
modelId: string;
|
|
25
|
-
percentLeft: number;
|
|
26
|
-
resetTime?: Date;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function readAuthPayload(value: string): GeminiAuthPayload {
|
|
30
|
-
try {
|
|
31
|
-
return JSON.parse(value) as GeminiAuthPayload;
|
|
32
|
-
} catch {
|
|
33
|
-
throw new LiveUsageUnavailableError("Gemini CLI OAuth login is required for live quota data");
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function parsePlan(tier: GeminiTier | undefined): string | undefined {
|
|
38
|
-
switch (tier) {
|
|
39
|
-
case "standard-tier":
|
|
40
|
-
return "Paid";
|
|
41
|
-
case "free-tier":
|
|
42
|
-
return "Free";
|
|
43
|
-
case "legacy-tier":
|
|
44
|
-
return "Legacy";
|
|
45
|
-
default:
|
|
46
|
-
return undefined;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function parseModelLabel(modelId: string): string {
|
|
51
|
-
const value = modelId.trim();
|
|
52
|
-
const slash = value.lastIndexOf("/");
|
|
53
|
-
return slash === -1 ? value : value.slice(slash + 1);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function isFlashLiteModel(id: string): boolean {
|
|
57
|
-
return id.includes("flash-lite");
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function isFlashModel(id: string): boolean {
|
|
61
|
-
return id.includes("flash") && !isFlashLiteModel(id);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function isProModel(id: string): boolean {
|
|
65
|
-
return id.includes("pro");
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function makeMeter(key: string, label: string, quota: GeminiQuota): LiveUsageItem {
|
|
69
|
-
return makeMeterItem({
|
|
70
|
-
key,
|
|
71
|
-
label,
|
|
72
|
-
usedPercent: clampPercent(100 - quota.percentLeft),
|
|
73
|
-
windowLabel: "24h",
|
|
74
|
-
resetsAt: quota.resetTime,
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
async function fetchJson(
|
|
79
|
-
url: string,
|
|
80
|
-
token: string,
|
|
81
|
-
body: Record<string, unknown>,
|
|
82
|
-
signal?: AbortSignal,
|
|
83
|
-
): Promise<Record<string, unknown>> {
|
|
84
|
-
const response = await fetch(url, {
|
|
85
|
-
method: "POST",
|
|
86
|
-
headers: {
|
|
87
|
-
Authorization: `Bearer ${token}`,
|
|
88
|
-
"Content-Type": "application/json",
|
|
89
|
-
},
|
|
90
|
-
body: JSON.stringify(body),
|
|
91
|
-
signal,
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
if (response.status === 401 || response.status === 403) {
|
|
95
|
-
throw new Error("Gemini CLI login expired. Re-run /login google-gemini-cli.");
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
if (!response.ok) {
|
|
99
|
-
const bodyText = await response.text().catch(() => "");
|
|
100
|
-
throw new Error(
|
|
101
|
-
bodyText.trim()
|
|
102
|
-
? `Gemini usage request failed (${response.status}): ${bodyText.trim()}`
|
|
103
|
-
: `Gemini usage request failed (${response.status}).`,
|
|
104
|
-
);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
return (await response.json()) as Record<string, unknown>;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
async function loadCodeAssistStatus(
|
|
111
|
-
token: string,
|
|
112
|
-
signal?: AbortSignal,
|
|
113
|
-
): Promise<{ tier?: GeminiTier }> {
|
|
114
|
-
try {
|
|
115
|
-
const payload = await fetchJson(
|
|
116
|
-
LOAD_CODE_ASSIST_URL,
|
|
117
|
-
token,
|
|
118
|
-
{ metadata: { ideType: "GEMINI_CLI", pluginType: "GEMINI" } },
|
|
119
|
-
signal,
|
|
120
|
-
);
|
|
121
|
-
|
|
122
|
-
const currentTier = payload.currentTier as Record<string, unknown> | undefined;
|
|
123
|
-
const tierId = readString(currentTier?.id);
|
|
124
|
-
|
|
125
|
-
return {
|
|
126
|
-
tier:
|
|
127
|
-
tierId === "free-tier" || tierId === "legacy-tier" || tierId === "standard-tier"
|
|
128
|
-
? tierId
|
|
129
|
-
: undefined,
|
|
130
|
-
};
|
|
131
|
-
} catch {
|
|
132
|
-
return {};
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function parseQuotas(payload: Record<string, unknown>): GeminiQuota[] {
|
|
137
|
-
const buckets = readArray<Record<string, unknown>>(payload.buckets);
|
|
138
|
-
const quotaByModel = new Map<string, GeminiQuota>();
|
|
139
|
-
|
|
140
|
-
for (const bucket of buckets) {
|
|
141
|
-
const input = bucket as Record<string, unknown> | null | undefined;
|
|
142
|
-
const modelId = readString(input?.modelId);
|
|
143
|
-
const remainingFraction =
|
|
144
|
-
typeof input?.remainingFraction === "number" ? input.remainingFraction : undefined;
|
|
145
|
-
if (!modelId || remainingFraction === undefined) continue;
|
|
146
|
-
|
|
147
|
-
const percentLeft = clampPercent(remainingFraction * 100);
|
|
148
|
-
const resetTime = parseISODate(readString(input?.resetTime));
|
|
149
|
-
const quota: GeminiQuota = {
|
|
150
|
-
modelId,
|
|
151
|
-
percentLeft,
|
|
152
|
-
resetTime,
|
|
153
|
-
};
|
|
154
|
-
|
|
155
|
-
const existing = quotaByModel.get(modelId);
|
|
156
|
-
if (!existing || quota.percentLeft < existing.percentLeft) {
|
|
157
|
-
quotaByModel.set(modelId, quota);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
return [...quotaByModel.values()].sort((left, right) =>
|
|
162
|
-
left.modelId.localeCompare(right.modelId),
|
|
163
|
-
);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function buildItems(quotas: GeminiQuota[]): LiveUsageItem[] {
|
|
167
|
-
const lower = quotas.map((quota) => ({ quota, modelId: quota.modelId.toLowerCase() }));
|
|
168
|
-
const pro = lower.filter((entry) => isProModel(entry.modelId)).map((entry) => entry.quota);
|
|
169
|
-
const flash = lower.filter((entry) => isFlashModel(entry.modelId)).map((entry) => entry.quota);
|
|
170
|
-
const flashLite = lower
|
|
171
|
-
.filter((entry) => isFlashLiteModel(entry.modelId))
|
|
172
|
-
.map((entry) => entry.quota);
|
|
173
|
-
|
|
174
|
-
const items: LiveUsageItem[] = [];
|
|
175
|
-
const proMin = pro.sort((left, right) => left.percentLeft - right.percentLeft)[0];
|
|
176
|
-
const flashMin = flash.sort((left, right) => left.percentLeft - right.percentLeft)[0];
|
|
177
|
-
const flashLiteMin = flashLite.sort((left, right) => left.percentLeft - right.percentLeft)[0];
|
|
178
|
-
|
|
179
|
-
if (proMin) items.push(makeMeter("pro", "Pro", proMin));
|
|
180
|
-
if (flashMin) items.push(makeMeter("flash", "Flash", flashMin));
|
|
181
|
-
if (flashLiteMin) items.push(makeMeter("flash-lite", "Flash Lite", flashLiteMin));
|
|
182
|
-
|
|
183
|
-
if (items.length > 0) return items;
|
|
184
|
-
|
|
185
|
-
for (const quota of quotas.slice(0, 3)) {
|
|
186
|
-
items.push(makeMeter(quota.modelId, parseModelLabel(quota.modelId), quota));
|
|
187
|
-
}
|
|
188
|
-
return items;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
async function fetchUsage(ctx: ExtensionContext, signal?: AbortSignal): Promise<LiveUsageSnapshot> {
|
|
192
|
-
const apiKey = await ctx.modelRegistry.getApiKeyForProvider(PROVIDER_ID);
|
|
193
|
-
if (!apiKey) throw new LiveUsageUnavailableError("no auth configured");
|
|
194
|
-
|
|
195
|
-
const auth = readAuthPayload(apiKey);
|
|
196
|
-
const token = typeof auth.token === "string" ? auth.token.trim() : "";
|
|
197
|
-
if (!token) {
|
|
198
|
-
throw new LiveUsageUnavailableError("Gemini CLI OAuth login is required for live quota data");
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
const authProjectId = typeof auth.projectId === "string" ? auth.projectId.trim() : undefined;
|
|
202
|
-
const statusPromise = loadCodeAssistStatus(token, signal);
|
|
203
|
-
const quotaPromise = fetchJson(
|
|
204
|
-
QUOTA_URL,
|
|
205
|
-
token,
|
|
206
|
-
authProjectId ? { project: authProjectId } : {},
|
|
207
|
-
signal,
|
|
208
|
-
);
|
|
209
|
-
|
|
210
|
-
const [status, quotaPayload] = await Promise.all([statusPromise, quotaPromise]);
|
|
211
|
-
const quotas = parseQuotas(quotaPayload);
|
|
212
|
-
|
|
213
|
-
if (quotas.length === 0) {
|
|
214
|
-
throw new Error("Gemini did not return any live quota data.");
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
return {
|
|
218
|
-
plan: parsePlan(status.tier),
|
|
219
|
-
fetchedAt: new Date(),
|
|
220
|
-
items: buildItems(quotas),
|
|
221
|
-
};
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
const googleGeminiCliProvider: UsageProviderDefinition = {
|
|
225
|
-
id: PROVIDER_ID,
|
|
226
|
-
label: "Gemini CLI",
|
|
227
|
-
shortLabel: "gemini",
|
|
228
|
-
color: { r: 171, g: 135, b: 234 },
|
|
229
|
-
fetchLiveUsage: fetchUsage,
|
|
230
|
-
};
|
|
231
|
-
|
|
232
|
-
export default googleGeminiCliProvider;
|