@buyi1net/pi-toolkit 0.0.1

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.
Files changed (129) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +45 -0
  3. package/assembler.ts +127 -0
  4. package/config.ts +201 -0
  5. package/i18n.ts +693 -0
  6. package/index.ts +99 -0
  7. package/menu/items.ts +264 -0
  8. package/menu/panels.ts +91 -0
  9. package/menu/settings-list.ts +84 -0
  10. package/menu/theme.ts +31 -0
  11. package/menu/toolkit-menu.ts +139 -0
  12. package/module.ts +156 -0
  13. package/modules/eyes/chain.ts +173 -0
  14. package/modules/eyes/config.ts +319 -0
  15. package/modules/eyes/index.ts +161 -0
  16. package/modules/eyes/menu.ts +828 -0
  17. package/modules/eyes/pi-model-backend.ts +245 -0
  18. package/modules/eyes/resilience.ts +161 -0
  19. package/modules/eyes/vision-bridge.ts +232 -0
  20. package/modules/eyes/vision-cache.ts +86 -0
  21. package/modules/eyes/vision-json.ts +19 -0
  22. package/modules/eyes/vision-preprocess.ts +192 -0
  23. package/modules/eyes/vision-probe.ts +21 -0
  24. package/modules/eyes/vision-prompt.ts +90 -0
  25. package/modules/eyes/vision-tool.ts +59 -0
  26. package/modules/eyes/vision-types.ts +33 -0
  27. package/modules/index.ts +10 -0
  28. package/modules/subagents/agents/researcher.md +51 -0
  29. package/modules/subagents/agents/scout.md +40 -0
  30. package/modules/subagents/agents/worker.md +79 -0
  31. package/modules/subagents/config.json.example +6 -0
  32. package/modules/subagents/config.ts +144 -0
  33. package/modules/subagents/index.ts +91 -0
  34. package/modules/subagents/menu.ts +388 -0
  35. package/modules/subagents/src/activity.ts +511 -0
  36. package/modules/subagents/src/agents.ts +126 -0
  37. package/modules/subagents/src/command.ts +37 -0
  38. package/modules/subagents/src/dependencies.ts +246 -0
  39. package/modules/subagents/src/diagnostics.ts +13 -0
  40. package/modules/subagents/src/display.ts +94 -0
  41. package/modules/subagents/src/headless.ts +342 -0
  42. package/modules/subagents/src/herdr.ts +203 -0
  43. package/modules/subagents/src/index.ts +2798 -0
  44. package/modules/subagents/src/inspect-tool.ts +839 -0
  45. package/modules/subagents/src/launch-config.ts +196 -0
  46. package/modules/subagents/src/layout-budget.ts +26 -0
  47. package/modules/subagents/src/list-tool.ts +292 -0
  48. package/modules/subagents/src/message-tool.ts +808 -0
  49. package/modules/subagents/src/names.ts +17 -0
  50. package/modules/subagents/src/pane-layout.ts +49 -0
  51. package/modules/subagents/src/params.ts +145 -0
  52. package/modules/subagents/src/renderers.ts +181 -0
  53. package/modules/subagents/src/result.ts +43 -0
  54. package/modules/subagents/src/retention.ts +114 -0
  55. package/modules/subagents/src/route-error.ts +212 -0
  56. package/modules/subagents/src/routing.ts +235 -0
  57. package/modules/subagents/src/runtime-registry.ts +159 -0
  58. package/modules/subagents/src/session.ts +801 -0
  59. package/modules/subagents/src/status.ts +513 -0
  60. package/modules/subagents/src/stop-tool.ts +235 -0
  61. package/modules/subagents/src/subagent-done.ts +590 -0
  62. package/modules/subagents/src/subagent-tool.ts +1155 -0
  63. package/modules/subagents/src/surface.ts +355 -0
  64. package/modules/subagents/src/team-dispatch-tool.ts +219 -0
  65. package/modules/subagents/src/team.ts +232 -0
  66. package/modules/subagents/src/tmux.ts +210 -0
  67. package/modules/subagents/src/tools/safe-bash.ts +72 -0
  68. package/modules/subagents/src/types.ts +131 -0
  69. package/modules/tui/adapter/provider-usage.ts +143 -0
  70. package/modules/tui/config.ts +50 -0
  71. package/modules/tui/index.ts +120 -0
  72. package/modules/tui/kernel/pkg/shared/grok-subscription.ts +169 -0
  73. package/modules/tui/kernel/pkg/shared/official-subscription.ts +237 -0
  74. package/modules/tui/kernel/pkg/shared/provider-catalog.ts +367 -0
  75. package/modules/tui/kernel/pkg/shared/provider-contracts.ts +150 -0
  76. package/modules/tui/kernel/pkg/shared/provider-display.ts +55 -0
  77. package/modules/tui/kernel/pkg/shared/provider-parsers.ts +171 -0
  78. package/modules/tui/kernel/pkg/shared/volcengine.ts +191 -0
  79. package/modules/tui/kernel/pkg/shared/zhipu.ts +149 -0
  80. package/modules/tui/kernel/pkg/usage-core/index.ts +335 -0
  81. package/modules/tui/kernel/pkg/usage-core/provider-routes.ts +187 -0
  82. package/modules/tui/kernel/pkg/usage-node/index.ts +635 -0
  83. package/modules/tui/kernel/pkg/usage-node/provider-usage.ts +388 -0
  84. package/modules/tui/kernel/usage-core.ts +2 -0
  85. package/modules/tui/kernel/usage-node.ts +2 -0
  86. package/modules/tui/menu.ts +418 -0
  87. package/modules/tui/plugin/editor.ts +396 -0
  88. package/modules/tui/plugin/footer.ts +161 -0
  89. package/modules/tui/plugin/index.ts +30 -0
  90. package/modules/tui/plugin/lifecycle.ts +637 -0
  91. package/modules/tui/plugin/package-order.ts +169 -0
  92. package/modules/tui/plugin/screen-transition.ts +203 -0
  93. package/modules/tui/plugin/settings-config.ts +297 -0
  94. package/modules/tui/plugin/status-sources.ts +49 -0
  95. package/modules/tui/plugin/transition-gate.ts +261 -0
  96. package/modules/tui/renderer/custom-header.ts +157 -0
  97. package/modules/tui/renderer/editor.ts +70 -0
  98. package/modules/tui/renderer/header.ts +72 -0
  99. package/modules/tui/renderer/icons.ts +149 -0
  100. package/modules/tui/renderer/pi-installer-logo.ts +194 -0
  101. package/modules/tui/status/auto-compaction.ts +67 -0
  102. package/modules/tui/status/project-status.ts +655 -0
  103. package/modules/tui/status/provider-status.ts +120 -0
  104. package/modules/tui/status/runtime-status.ts +307 -0
  105. package/modules/tui/status/session-status.ts +325 -0
  106. package/modules/tui/status/status-config.ts +95 -0
  107. package/modules/tui/status/status-segments.ts +263 -0
  108. package/modules/tui/status/turn-telemetry.ts +466 -0
  109. package/modules/tui/themes/LICENSE.pi-themes-bundle +21 -0
  110. package/modules/tui/themes/UPSTREAM.md +7 -0
  111. package/modules/tui/themes/catppuccin-latte.json +80 -0
  112. package/modules/tui/themes/catppuccin-mocha.json +79 -0
  113. package/modules/tui/themes/crimson-noir.json +85 -0
  114. package/modules/tui/themes/dracula.json +79 -0
  115. package/modules/tui/themes/everforest-dark.json +85 -0
  116. package/modules/tui/themes/gruvbox-dark.json +85 -0
  117. package/modules/tui/themes/gruvbox-light.json +85 -0
  118. package/modules/tui/themes/matrix.json +85 -0
  119. package/modules/tui/themes/nord.json +85 -0
  120. package/modules/tui/themes/one-dark.json +85 -0
  121. package/modules/tui/themes/rose-pine-dawn.json +85 -0
  122. package/modules/tui/themes/rose-pine.json +85 -0
  123. package/modules/tui/themes/solarized-dark.json +85 -0
  124. package/modules/tui/themes/solarized-light.json +85 -0
  125. package/modules/tui/themes/tokyo-night-storm.json +79 -0
  126. package/modules/tui/themes/tokyo-night.json +79 -0
  127. package/package.json +28 -0
  128. package/services.ts +38 -0
  129. package/toolkit.ts +147 -0
@@ -0,0 +1,143 @@
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ import type {
5
+ ProviderAccess,
6
+ ProviderAccessOptions,
7
+ ProviderQueryAccess,
8
+ } from "../kernel/usage-core.ts";
9
+ import {
10
+ createProviderAccess,
11
+ FileUsageSnapshotCache,
12
+ findProviderQueryConfig,
13
+ UsageRuntime,
14
+ type UsageRuntimeState,
15
+ } from "../kernel/usage-node.ts";
16
+
17
+ type PiModel = NonNullable<ExtensionContext["model"]>;
18
+ type AccessResolver = (
19
+ ctx: ExtensionContext,
20
+ model: PiModel | undefined,
21
+ config?: PiProviderAccessConfig,
22
+ ) => Promise<ProviderAccess | null>;
23
+
24
+ export interface PiProviderAccessConfig {
25
+ queries?: readonly ProviderQueryAccess[];
26
+ credentials?: ProviderAccessOptions["credentials"];
27
+ githubDomain?: string;
28
+ }
29
+
30
+ const OPENAI_AUTH_CLAIM = "https://api.openai.com/auth";
31
+
32
+ function extractCodexAccountId(providerId: string, credential: string): string | undefined {
33
+ if (providerId !== "openai-codex") return undefined;
34
+ const payload = credential.split(".")[1];
35
+ if (!payload) return undefined;
36
+ try {
37
+ const claims: unknown = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
38
+ if (!claims || typeof claims !== "object") return undefined;
39
+ const auth = (claims as Record<string, unknown>)[OPENAI_AUTH_CLAIM];
40
+ if (!auth || typeof auth !== "object") return undefined;
41
+ const accountId = (auth as Record<string, unknown>).chatgpt_account_id;
42
+ return typeof accountId === "string" && accountId.trim() ? accountId : undefined;
43
+ } catch {
44
+ return undefined;
45
+ }
46
+ }
47
+
48
+ export async function resolvePiProviderAccess(
49
+ ctx: ExtensionContext,
50
+ model: PiModel | undefined = ctx.model,
51
+ config: PiProviderAccessConfig = {},
52
+ ): Promise<ProviderAccess | null> {
53
+ if (!model) return null;
54
+ try {
55
+ const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
56
+ if (!auth.ok) return null;
57
+ const credential = auth.apiKey ?? "";
58
+ const endpoint = auth.baseUrl ?? model.baseUrl;
59
+ return createProviderAccess({
60
+ providerId: model.provider,
61
+ modelId: model.id,
62
+ endpoint,
63
+ credential,
64
+ authKind: ctx.modelRegistry.isUsingOAuth(model) ? "oauth" : "api-key",
65
+ accountId: extractCodexAccountId(model.provider, credential),
66
+ githubDomain: config.githubDomain,
67
+ query: findProviderQueryConfig(endpoint, config.queries ?? []),
68
+ credentials: config.credentials,
69
+ });
70
+ } catch {
71
+ return null;
72
+ }
73
+ }
74
+
75
+ export class PiProviderUsageController {
76
+ private readonly ctx: ExtensionContext;
77
+ private readonly runtime: UsageRuntime;
78
+ private readonly resolveAccess: AccessResolver;
79
+ private readonly accessConfig: PiProviderAccessConfig;
80
+ private readonly refreshMs: number;
81
+ private interval: NodeJS.Timeout | undefined;
82
+ private activeModelKey = "";
83
+ private refreshEpoch = 0;
84
+ private disposed = false;
85
+
86
+ constructor(
87
+ ctx: ExtensionContext,
88
+ onChange: (state: UsageRuntimeState) => void,
89
+ options: {
90
+ refreshMs?: number;
91
+ resolveAccess?: AccessResolver;
92
+ accessConfig?: PiProviderAccessConfig;
93
+ runtime?: UsageRuntime;
94
+ } = {},
95
+ ) {
96
+ this.ctx = ctx;
97
+ this.refreshMs = options.refreshMs ?? 60_000;
98
+ this.resolveAccess = options.resolveAccess ?? resolvePiProviderAccess;
99
+ this.accessConfig = options.accessConfig ?? {};
100
+ this.runtime = options.runtime ?? new UsageRuntime({
101
+ userAgent: "pi-tui",
102
+ onChange,
103
+ cache: new FileUsageSnapshotCache(join(homedir(), ".pi", "agent", "cache", "pi-tui", "usage")),
104
+ });
105
+ }
106
+
107
+ getState(): UsageRuntimeState {
108
+ return this.runtime.getState();
109
+ }
110
+
111
+ start(): Promise<void> {
112
+ if (this.disposed || this.interval) return Promise.resolve();
113
+ const initialRefresh = this.refresh();
114
+ this.interval = setInterval(() => void this.refresh(), this.refreshMs);
115
+ this.interval.unref?.();
116
+ return initialRefresh;
117
+ }
118
+
119
+ async refresh(model: PiModel | undefined = this.ctx.model): Promise<void> {
120
+ if (this.disposed) return;
121
+ const epoch = ++this.refreshEpoch;
122
+ const modelKey = model ? `${model.provider}\0${model.id}\0${model.baseUrl}` : "";
123
+ if (modelKey !== this.activeModelKey) {
124
+ this.activeModelKey = modelKey;
125
+ // 凭据是异步解析的:身份一变先交运行层隔离旧供应商数据,解析期间不显示旧快照
126
+ this.runtime.begin(model
127
+ ? { providerId: model.provider, modelId: model.id, endpoint: model.baseUrl }
128
+ : null);
129
+ }
130
+ const access = await this.resolveAccess(this.ctx, model, this.accessConfig);
131
+ // 解析期间有更新的 refresh 进场(换模型或换账号):本次结果已过期,由最新一次 refresh 提交
132
+ if (this.disposed || epoch !== this.refreshEpoch) return;
133
+ await this.runtime.commit(access);
134
+ }
135
+
136
+ dispose(): void {
137
+ if (this.disposed) return;
138
+ this.disposed = true;
139
+ if (this.interval) clearInterval(this.interval);
140
+ this.interval = undefined;
141
+ this.runtime.dispose();
142
+ }
143
+ }
@@ -0,0 +1,50 @@
1
+ // pi-tui 模块的配置落点:pi-toolkit.json 的 `modules.tui` 节。
2
+ //
3
+ // 节结构(与迁入前的 pi-tui.json 外观语义一致,只是换了宿主文件):
4
+ // modules.tui.appearance.editor / header / footer
5
+ // modules.tui.status.preset / segments
6
+ // modules.tui.data.providerRefreshMs / telemetry
7
+ // modules.tui.advanced.spinner
8
+ // 供应商查询凭据不在这里:按用户裁决留在独立文件 <agentDir>/pi-tui.json,
9
+ // 本模块只读(见 plugin/settings-config.ts),toolkit 永不写它。
10
+ //
11
+ // 写盘走骨架的 saveToolkitConfig:深合并、剥敏感键、0600 原子替换。
12
+
13
+ import { getAgentDir } from "@earendil-works/pi-coding-agent";
14
+ import { getToolkitConfigPath, saveToolkitConfig } from "../../config.ts";
15
+ import type { ModuleConfigRecord } from "../../module.ts";
16
+ import {
17
+ parseTuiSection,
18
+ type LoadedPiTuiConfig,
19
+ type PiTuiConfig,
20
+ } from "./plugin/settings-config.ts";
21
+
22
+ /** 模块 id:同时是配置节名与菜单项 id 前缀 */
23
+ export const TUI_MODULE_ID = "tui";
24
+ export const TUI_SECTION_LABEL = `modules.${TUI_MODULE_ID}`;
25
+
26
+ export type TuiSectionUpdate = {
27
+ appearance?: Partial<PiTuiConfig["appearance"]>;
28
+ status?: Partial<PiTuiConfig["status"]>;
29
+ data?: Partial<Pick<PiTuiConfig["data"], "providerRefreshMs" | "telemetry">>;
30
+ advanced?: Partial<PiTuiConfig["advanced"]>;
31
+ };
32
+
33
+ /**
34
+ * 读 `modules.tui` 节并规范化。非法取值逐条记 warning 并回退默认值,
35
+ * 菜单展示与 lifecycle 的实际安装都用这一份结果。
36
+ */
37
+ export function readTuiSection(section: Record<string, unknown> | undefined): LoadedPiTuiConfig {
38
+ return parseTuiSection(section);
39
+ }
40
+
41
+ /** 写本节点(深合并,不动其它模块节与语言设置)。 */
42
+ export async function saveTuiSection(
43
+ update: TuiSectionUpdate,
44
+ options: { readonly agentDir?: string } = {},
45
+ ): Promise<void> {
46
+ const path = getToolkitConfigPath(options.agentDir ?? getAgentDir());
47
+ await saveToolkitConfig(path, {
48
+ modules: { [TUI_MODULE_ID]: update as ModuleConfigRecord },
49
+ });
50
+ }
@@ -0,0 +1,120 @@
1
+ // pi-tui 模块:把 pi-tui 的 pi 宿主侧(编辑器边框、Header、Footer、转场、
2
+ // 遥测、状态数据源、主题、配置)装进 pi-toolkit。
3
+ //
4
+ // 迁入原则是搬移优先于重写:`plugin/`、`renderer/`、`status/`、`adapter/` 是原
5
+ // pi-src 的实现(只把对 `packages/usage-*` 的 import 改走 `kernel/` 转发),
6
+ // `themes/` 是随包注册的主题 JSON。`claude-src/` 与 `packages/` 共享内核留在原仓,
7
+ // 继续支撑 claude-line 独立发布线(规格说明决策 2)。
8
+ //
9
+ // 本文件只做装配:
10
+ // - 15 个事件钩子、常驻 UI(编辑器 / Header / Footer / 工作指示动画)由
11
+ // registerPiTuiLifecycle 按配置安装;enabled=false 时装配器根本不调用 register
12
+ // - 外观 / 状态 / 数据 / 高级四个子节读写 pi-toolkit.json 的 `modules.tui` 节
13
+ // - 供应商查询凭据只读独立文件 <agentDir>/pi-tui.json(用户裁决),永不写回
14
+ // - 服务句柄 `ui.provider-usage`:供其它模块查询供应商余额/套餐运行态(本单只注册)
15
+
16
+ import { getAgentDir } from "@earendil-works/pi-coding-agent";
17
+ import { enabledField, type ModuleContext, type ModuleDefinition } from "../../module.ts";
18
+ import { TUI_MODULE_ID, readTuiSection } from "./config.ts";
19
+ import type { UsageRuntimeState } from "./kernel/usage-node.ts";
20
+ import { buildTuiMenuItems, TUI_MENU_ITEM_ID, type TuiMenuRuntime } from "./menu.ts";
21
+ import { registerPiTuiLifecycle, type PiTuiLifecycleHandle } from "./plugin/lifecycle.ts";import type { VisibleScreenOutput } from "./plugin/screen-transition.ts";
22
+ import { loadProviderAccessFile, type LoadedPiTuiConfig } from "./plugin/settings-config.ts";
23
+
24
+ export { TUI_MODULE_ID } from "./config.ts";
25
+ export { TUI_MENU_ITEM_ID, TuiPanel } from "./menu.ts";
26
+
27
+ /** 服务句柄名(注册表要求全小写) */
28
+ export const TUI_PROVIDER_SERVICE_NAME = "ui.provider-usage";
29
+
30
+ /**
31
+ * 供应商余额/套餐查询服务句柄:与编辑器状态栏同一份运行态数据源。
32
+ * 本单只注册不做联动;常驻 UI 未安装(enabled=false 或非 tui 会话)时
33
+ * snapshot() 返回 undefined,refresh() 是空操作。
34
+ */
35
+ export interface TuiProviderUsageService {
36
+ readonly id: "tui";
37
+ snapshot(): UsageRuntimeState | undefined;
38
+ refresh(): Promise<void>;
39
+ }
40
+
41
+ export interface TuiModuleOptions {
42
+ /** pi 的配置目录(pi-toolkit.json 与只读的 pi-tui.json 都在它下面);默认 getAgentDir() */
43
+ readonly agentDir?: string;
44
+ /** 终端的可见屏幕输出;默认 process.stdout */
45
+ readonly output?: VisibleScreenOutput;
46
+ }
47
+
48
+ /** 自测钩子:最近一次 register 交回的 lifecycle 把手(生产代码不读) */
49
+ export const __test__: { handle?: PiTuiLifecycleHandle } = {};
50
+
51
+ /** 本模块认识的配置 = modules.tui 节 + 独立凭据文件里的 providerAccess */
52
+ function loadModuleConfig(context: ModuleContext, agentDir: string): LoadedPiTuiConfig {
53
+ const section = readTuiSection(context.getConfig());
54
+ const provider = loadProviderAccessFile(agentDir);
55
+ return {
56
+ config: {
57
+ appearance: section.config.appearance,
58
+ status: section.config.status,
59
+ data: {
60
+ ...section.config.data,
61
+ ...(provider.access ? { providerAccess: provider.access } : {}),
62
+ },
63
+ advanced: section.config.advanced,
64
+ },
65
+ warnings: [...section.warnings, ...provider.warnings],
66
+ };
67
+ }
68
+
69
+ function registerTui(
70
+ context: ModuleContext,
71
+ options: TuiModuleOptions,
72
+ runtime: TuiMenuRuntime,
73
+ ): void {
74
+ const agentDir = options.agentDir ?? getAgentDir();
75
+
76
+ // 宿主注入:配置完全来自本模块(modules.tui 节 + pi-tui.json 凭据文件),
77
+ // lifecycle 不再自己找配置文件;保存后由 TuiMenuRuntime.applyUi 触发原子重装。
78
+ const handle = registerPiTuiLifecycle(context.pi, options.output ?? process.stdout, {
79
+ agentDir,
80
+ loadConfig: () => loadModuleConfig(context, agentDir),
81
+ t: context.t,
82
+ });
83
+ __test__.handle = handle;
84
+
85
+ runtime.reload = () => context.reloadConfig();
86
+ runtime.applyUi = () => handle.applyConfig();
87
+
88
+ context.services.register(TUI_PROVIDER_SERVICE_NAME, {
89
+ id: "tui",
90
+ snapshot: () => handle.getProviderUsage()?.getState(),
91
+ refresh: async () => {
92
+ await handle.getProviderUsage()?.refresh();
93
+ },
94
+ } satisfies TuiProviderUsageService);
95
+ }
96
+
97
+ export function createTuiModule(options: TuiModuleOptions = {}): ModuleDefinition {
98
+ // 菜单写盘后要让状态中枢重新读盘,并让 lifecycle 重装常驻 UI
99
+ const runtime: TuiMenuRuntime = { reload: async () => {}, applyUi: () => false };
100
+
101
+ return {
102
+ id: TUI_MODULE_ID,
103
+ labelKey: "module.tui.label",
104
+ descriptionKey: "module.tui.description",
105
+ group: "general",
106
+ // schema 只放总开关;外观/状态/数据/高级是结构化配置(非 schema 键),
107
+ // 由本模块的菜单与配置层读写。
108
+ configSchema: {
109
+ enabled: enabledField("module.tui.enabled.label", "module.tui.enabled.description"),
110
+ },
111
+ register(context): void {
112
+ registerTui(context, options, runtime);
113
+ },
114
+ menuItems(context): ReturnType<typeof buildTuiMenuItems> {
115
+ return buildTuiMenuItems(context, runtime);
116
+ },
117
+ };
118
+ }
119
+
120
+ export const tuiModule: ModuleDefinition = createTuiModule();
@@ -0,0 +1,169 @@
1
+ // xAI / Grok 官方订阅额度。grok.com 使用 gRPC-web + protobuf,且未公开 .proto;
2
+ // 此处保留 cc-switch 0b5da51 的防御式字段扫描策略,不把二进制响应当 JSON 猜测。
3
+
4
+ import type { ProviderUsage } from './provider-contracts.ts';
5
+
6
+ const ENDPOINT = 'https://grok.com/grok_api_v2.GrokBuildBilling/GetGrokCreditsConfig';
7
+
8
+ interface Scan {
9
+ floats: Array<{ path: number[]; value: number; order: number }>;
10
+ varints: Array<{ path: number[]; value: number }>;
11
+ }
12
+
13
+ function readVarint(bytes: Uint8Array, state: { index: number }): number | null {
14
+ let value = 0;
15
+ let shift = 0;
16
+ while (state.index < bytes.length && shift < 53) {
17
+ const byte = bytes[state.index++];
18
+ value += (byte & 0x7f) * 2 ** shift;
19
+ if ((byte & 0x80) === 0) return value;
20
+ shift += 7;
21
+ }
22
+ return null;
23
+ }
24
+
25
+ function scanProtobuf(
26
+ bytes: Uint8Array,
27
+ depth: number,
28
+ path: number[],
29
+ scan: Scan,
30
+ order: { value: number },
31
+ ): void {
32
+ const state = { index: 0 };
33
+ while (state.index < bytes.length) {
34
+ const start = state.index;
35
+ const key = readVarint(bytes, state);
36
+ if (!key) {
37
+ state.index = start + 1;
38
+ continue;
39
+ }
40
+ const field = Math.floor(key / 8);
41
+ const wire = key & 7;
42
+ const fieldPath = [...path, field];
43
+ if (wire === 0) {
44
+ const value = readVarint(bytes, state);
45
+ if (value == null) state.index = start + 1;
46
+ else scan.varints.push({ path: fieldPath, value });
47
+ } else if (wire === 1) {
48
+ state.index += 8;
49
+ } else if (wire === 2) {
50
+ const length = readVarint(bytes, state);
51
+ if (length == null || state.index + length > bytes.length) {
52
+ state.index = start + 1;
53
+ continue;
54
+ }
55
+ const end = state.index + length;
56
+ if (depth < 4) scanProtobuf(bytes.subarray(state.index, end), depth + 1, fieldPath, scan, order);
57
+ state.index = end;
58
+ } else if (wire === 5) {
59
+ if (state.index + 4 > bytes.length) break;
60
+ const view = new DataView(bytes.buffer, bytes.byteOffset + state.index, 4);
61
+ scan.floats.push({ path: fieldPath, value: view.getFloat32(0, true), order: order.value++ });
62
+ state.index += 4;
63
+ } else {
64
+ state.index = start + 1;
65
+ }
66
+ }
67
+ }
68
+
69
+ function grpcPayloads(bytes: Uint8Array): Uint8Array[] {
70
+ const payloads: Uint8Array[] = [];
71
+ let index = 0;
72
+ while (index < bytes.length) {
73
+ if (index + 5 > bytes.length) return [];
74
+ const flags = bytes[index];
75
+ const length = new DataView(bytes.buffer, bytes.byteOffset + index + 1, 4).getUint32(0, false);
76
+ const start = index + 5;
77
+ const end = start + length;
78
+ if (end > bytes.length) return [];
79
+ if ((flags & 0x80) === 0) payloads.push(bytes.subarray(start, end));
80
+ index = end;
81
+ }
82
+ return payloads;
83
+ }
84
+
85
+ function looksLikeProtobuf(bytes: Uint8Array): boolean {
86
+ const first = bytes[0];
87
+ return bytes.length > 0 && first >> 3 > 0 && [0, 1, 2, 5].includes(first & 7);
88
+ }
89
+
90
+ function samePath(left: number[], right: number[]): boolean {
91
+ return left.length === right.length && left.every((value, index) => value === right[index]);
92
+ }
93
+
94
+ function tierLabel(resetSeconds: number | null, nowSeconds: number): string {
95
+ if (resetSeconds != null) {
96
+ const days = Math.round((resetSeconds - nowSeconds) / 86_400);
97
+ if (days >= 4 && days <= 12) return '7d';
98
+ if (days >= 20 && days <= 45) return '30d';
99
+ }
100
+ return 'Credits';
101
+ }
102
+
103
+ export function parseGrokSubscription(
104
+ bytes: Uint8Array,
105
+ nowSeconds = Math.floor(Date.now() / 1000),
106
+ ): ProviderUsage | null {
107
+ let payloads = grpcPayloads(bytes);
108
+ if (!payloads.length && looksLikeProtobuf(bytes)) payloads = [bytes];
109
+ if (!payloads.length) return null;
110
+
111
+ const scan: Scan = { floats: [], varints: [] };
112
+ for (const payload of payloads) scanProtobuf(payload, 0, [], scan, { value: 0 });
113
+ const percent = scan.floats
114
+ .filter((item) => item.path.at(-1) === 1 && Number.isFinite(item.value) && item.value >= 0 && item.value <= 100)
115
+ .sort((left, right) => left.path.length - right.path.length || left.order - right.order)[0]?.value;
116
+ const resets = scan.varints
117
+ .filter((item) => item.value >= 1_700_000_000 && item.value <= 2_100_000_000 && item.value > nowSeconds)
118
+ .sort((left, right) => {
119
+ const leftExact = samePath(left.path, [1, 5, 1]) ? 0 : 1;
120
+ const rightExact = samePath(right.path, [1, 5, 1]) ? 0 : 1;
121
+ return leftExact - rightExact || left.value - right.value;
122
+ });
123
+ const reset = resets[0]?.value ?? null;
124
+ const hasPeriod = scan.varints.some(
125
+ (item) =>
126
+ (item.path[0] === 1 && item.path[1] === 6) ||
127
+ (samePath(item.path, [1, 8, 1]) && (item.value === 1 || item.value === 2)),
128
+ );
129
+ const used = percent ?? (!scan.floats.length && reset != null && hasPeriod ? 0 : null);
130
+ if (used == null) return null;
131
+ return {
132
+ mode: 'subscription',
133
+ quota: {
134
+ provider: 'xai',
135
+ windows: [
136
+ {
137
+ label: tierLabel(reset, nowSeconds),
138
+ remainingPercent: Math.max(0, Math.min(100, 100 - used)),
139
+ resetMs: reset == null ? null : reset * 1000,
140
+ },
141
+ ],
142
+ },
143
+ };
144
+ }
145
+
146
+ export async function fetchGrokSubscription(
147
+ accessToken: string,
148
+ request: typeof fetch = fetch,
149
+ userAgent?: string,
150
+ ): Promise<ProviderUsage | null> {
151
+ if (!accessToken) return null;
152
+ const response = await request(ENDPOINT, {
153
+ method: 'POST',
154
+ headers: {
155
+ Authorization: `Bearer ${accessToken}`,
156
+ Origin: 'https://grok.com',
157
+ Referer: 'https://grok.com/?_s=usage',
158
+ Accept: '*/*',
159
+ 'Content-Type': 'application/grpc-web+proto',
160
+ 'x-grpc-web': '1',
161
+ 'x-user-agent': 'connect-es/2.1.1',
162
+ ...(userAgent ? { 'User-Agent': userAgent } : {}),
163
+ },
164
+ body: new Uint8Array(5),
165
+ signal: AbortSignal.timeout(15_000),
166
+ });
167
+ if (!response.ok || (response.headers.get('grpc-status') ?? '0') !== '0') return null;
168
+ return parseGrokSubscription(new Uint8Array(await response.arrayBuffer()));
169
+ }