chatccc 0.2.177 → 0.2.179

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.
@@ -6,8 +6,9 @@ import { join } from "node:path";
6
6
  import type { PlatformAdapter } from "../platform-adapter.ts";
7
7
  import type { SessionInfo, ToolAdapter } from "../adapters/adapter-interface.ts";
8
8
 
9
- const mockStreamStates = new Map<string, { status: "running" | "done" | "stopped"; finalReply: string }>();
10
- const mockGetCodexUsageSummary = vi.hoisted(() => vi.fn());
9
+ const mockStreamStates = new Map<string, { status: "running" | "done" | "stopped"; finalReply: string }>();
10
+ const mockGetCodexUsageSummary = vi.hoisted(() => vi.fn());
11
+ const mockGetCursorUsageSummary = vi.hoisted(() => vi.fn());
11
12
 
12
13
  vi.mock("../im-skills.ts", () => ({
13
14
  buildImSkillsPrompt: async () => "",
@@ -15,7 +16,7 @@ vi.mock("../im-skills.ts", () => ({
15
16
  exportSkillSubDocs: async () => {},
16
17
  }));
17
18
 
18
- vi.mock("../stream-state.ts", () => ({
19
+ vi.mock("../stream-state.ts", () => ({
19
20
  readStreamState: async (sessionId: string) => {
20
21
  const state = mockStreamStates.get(sessionId);
21
22
  if (!state) return null;
@@ -50,14 +51,18 @@ vi.mock("../stream-state.ts", () => ({
50
51
  cwd,
51
52
  tool,
52
53
  }),
53
- fixStaleStreamStates: async () => {},
54
- }));
55
-
56
- vi.mock("../feishu-platform.ts", () => ({
57
- getCodexUsageSummary: mockGetCodexUsageSummary,
58
- getTenantAccessToken: vi.fn(async () => "tenant-token"),
59
- sendPostMessage: vi.fn(async () => true),
60
- }));
54
+ fixStaleStreamStates: async () => {},
55
+ }));
56
+
57
+ vi.mock("../feishu-platform.ts", () => ({
58
+ getCodexUsageSummary: mockGetCodexUsageSummary,
59
+ getTenantAccessToken: vi.fn(async () => "tenant-token"),
60
+ sendPostMessage: vi.fn(async () => true),
61
+ }));
62
+
63
+ vi.mock("../cursor-usage.ts", () => ({
64
+ getCursorUsageSummary: mockGetCursorUsageSummary,
65
+ }));
61
66
 
62
67
  import { handleCommand } from "../orchestrator.ts";
63
68
  import {
@@ -120,14 +125,40 @@ describe("handleCommand WeChat processing ack", () => {
120
125
  _setSessionToolsFileForTest(join(tempDir, "sessions.json"));
121
126
  resetState();
122
127
  resetBindingState();
123
- mockStreamStates.clear();
124
- mockGetCodexUsageSummary.mockReset();
125
- mockGetCodexUsageSummary.mockResolvedValue({
126
- fiveHour: { usedPercent: 0, remainingPercent: 100, resetAtEpochSeconds: null, resetAfterSeconds: null },
127
- weekly: { usedPercent: 0, remainingPercent: 100, resetAtEpochSeconds: null, resetAfterSeconds: null },
128
- });
129
- _setAdapterForToolForTest("claude", mockAdapter());
130
- });
128
+ mockStreamStates.clear();
129
+ mockGetCodexUsageSummary.mockReset();
130
+ mockGetCursorUsageSummary.mockReset();
131
+ mockGetCodexUsageSummary.mockResolvedValue({
132
+ fiveHour: { usedPercent: 0, remainingPercent: 100, resetAtEpochSeconds: null, resetAfterSeconds: null },
133
+ weekly: { usedPercent: 0, remainingPercent: 100, resetAtEpochSeconds: null, resetAfterSeconds: null },
134
+ });
135
+ mockGetCursorUsageSummary.mockResolvedValue({
136
+ billingCycleStart: "1779357999000",
137
+ billingCycleEnd: "1782036399000",
138
+ planUsage: {
139
+ totalSpend: 8159,
140
+ includedSpend: 2000,
141
+ bonusSpend: 6159,
142
+ limit: 2000,
143
+ remainingBonus: false,
144
+ autoPercentUsed: 0,
145
+ apiPercentUsed: 100,
146
+ totalPercentUsed: 100,
147
+ },
148
+ spendLimitUsage: {
149
+ pooledLimit: 48950000,
150
+ pooledUsed: 31808224,
151
+ pooledRemaining: 17141776,
152
+ individualUsed: 101252,
153
+ limitType: "team",
154
+ },
155
+ displayThreshold: 200,
156
+ enabled: true,
157
+ displayMessage: "You've hit your usage limit",
158
+ autoBucketModels: ["default"],
159
+ });
160
+ _setAdapterForToolForTest("claude", mockAdapter());
161
+ });
131
162
 
132
163
  afterEach(async () => {
133
164
  resetState();
@@ -249,7 +280,7 @@ describe("handleCommand WeChat processing ack", () => {
249
280
  expect(registry["feishu-group"]?.sessionId).toBe("sid-feishu-new");
250
281
  });
251
282
 
252
- it("cleans stale Feishu p2p binding but keeps valid commands from auto-creating a group", async () => {
283
+ it("cleans stale Feishu p2p binding but keeps valid commands from auto-creating a group", async () => {
253
284
  const platform = mockPlatform("feishu");
254
285
  await recordSessionRegistry({
255
286
  chatId: "feishu-p2p",
@@ -264,76 +295,115 @@ describe("handleCommand WeChat processing ack", () => {
264
295
  expect(platform.createGroup).not.toHaveBeenCalled();
265
296
  expect(platform.sendRawCard).toHaveBeenCalled();
266
297
  const registry = await loadSessionRegistryForBinding();
267
- expect(registry["feishu-p2p"]).toBeUndefined();
268
- });
269
-
270
- it("handles /usage without creating a new Feishu group", async () => {
271
- const platform = mockPlatform("feishu");
272
- mockGetCodexUsageSummary.mockResolvedValue({
273
- fiveHour: { usedPercent: 37, remainingPercent: 63, resetAtEpochSeconds: 1781528212, resetAfterSeconds: 10349 },
274
- weekly: { usedPercent: 12, remainingPercent: 88, resetAtEpochSeconds: 1781842926, resetAfterSeconds: 325063 },
275
- });
276
-
277
- await handleCommand(platform, "/usage", "feishu-p2p", "ou-user", Date.now(), "p2p");
278
-
279
- expect(platform.createGroup).not.toHaveBeenCalled();
280
- expect(platform.sendCard).toHaveBeenCalledWith(
281
- "feishu-p2p",
282
- "Codex Usage",
283
- expect.stringContaining("**5h:** 已用 37%,剩余 63%,重置:"),
284
- "blue",
285
- );
286
- expect(platform.sendCard).toHaveBeenCalledWith(
287
- "feishu-p2p",
288
- "Codex Usage",
289
- expect.stringContaining("约 2小时52分钟后"),
290
- "blue",
291
- );
292
- expect(platform.sendCard).toHaveBeenCalledWith(
293
- "feishu-p2p",
294
- "Codex Usage",
295
- expect.stringContaining("[███████░░░░░░░░░░░░░]"),
296
- "blue",
297
- );
298
- expect(platform.sendCard).toHaveBeenCalledWith(
299
- "feishu-p2p",
300
- "Codex Usage",
301
- expect.stringContaining("**周:** 已用 12%,剩余 88%,重置:"),
302
- "blue",
303
- );
304
- expect(platform.sendCard).toHaveBeenCalledWith(
305
- "feishu-p2p",
306
- "Codex Usage",
307
- expect.stringContaining("约 3天18小时17分钟后"),
308
- "blue",
309
- );
310
- expect(platform.sendCard).toHaveBeenCalledWith(
311
- "feishu-p2p",
312
- "Codex Usage",
313
- expect.stringContaining("[██░░░░░░░░░░░░░░░░░░]"),
314
- "blue",
315
- );
316
- });
317
-
318
- it("only advertises /usage in new Codex session ready messages", async () => {
319
- const codexPlatform = mockPlatform("feishu");
320
- _setAdapterForToolForTest("codex", mockAdapter("sid-codex"));
321
-
322
- await handleCommand(codexPlatform, "/new codex", "feishu-p2p", "ou-user", Date.now(), "p2p");
323
-
324
- expect(codexPlatform.sendCard).toHaveBeenCalledWith(
325
- "feishu-group",
326
- "Codex Session Ready",
327
- expect.stringContaining("发送 **/usage** 查看 Codex 5h 和周用量。"),
328
- "green",
329
- );
330
-
331
- const claudePlatform = mockPlatform("feishu");
332
- await handleCommand(claudePlatform, "/new claude", "feishu-p2p-2", "ou-user", Date.now(), "p2p");
333
-
334
- const claudeReadyCall = vi.mocked(claudePlatform.sendCard).mock.calls.find(
335
- ([chatId, title]) => chatId === "feishu-group" && title === "Claude Code Session Ready",
336
- );
337
- expect(claudeReadyCall?.[2]).not.toContain("/usage");
338
- });
339
- });
298
+ expect(registry["feishu-p2p"]).toBeUndefined();
299
+ });
300
+
301
+ it("handles /usage without creating a new Feishu group", async () => {
302
+ const platform = mockPlatform("feishu");
303
+ mockGetCodexUsageSummary.mockResolvedValue({
304
+ fiveHour: { usedPercent: 37, remainingPercent: 63, resetAtEpochSeconds: 1781528212, resetAfterSeconds: 10349 },
305
+ weekly: { usedPercent: 12, remainingPercent: 88, resetAtEpochSeconds: 1781842926, resetAfterSeconds: 325063 },
306
+ });
307
+
308
+ await handleCommand(platform, "/usage", "feishu-p2p", "ou-user", Date.now(), "p2p");
309
+
310
+ expect(platform.createGroup).not.toHaveBeenCalled();
311
+ expect(platform.sendCard).toHaveBeenCalledWith(
312
+ "feishu-p2p",
313
+ "Codex Usage",
314
+ expect.stringContaining("**5h:** 已用 37%,剩余 63%,重置:"),
315
+ "blue",
316
+ );
317
+ expect(platform.sendCard).toHaveBeenCalledWith(
318
+ "feishu-p2p",
319
+ "Codex Usage",
320
+ expect.stringContaining("约 2小时52分钟后"),
321
+ "blue",
322
+ );
323
+ expect(platform.sendCard).toHaveBeenCalledWith(
324
+ "feishu-p2p",
325
+ "Codex Usage",
326
+ expect.stringContaining("[███████░░░░░░░░░░░░░]"),
327
+ "blue",
328
+ );
329
+ expect(platform.sendCard).toHaveBeenCalledWith(
330
+ "feishu-p2p",
331
+ "Codex Usage",
332
+ expect.stringContaining("**周:** 已用 12%,剩余 88%,重置:"),
333
+ "blue",
334
+ );
335
+ expect(platform.sendCard).toHaveBeenCalledWith(
336
+ "feishu-p2p",
337
+ "Codex Usage",
338
+ expect.stringContaining("约 3天18小时17分钟后"),
339
+ "blue",
340
+ );
341
+ expect(platform.sendCard).toHaveBeenCalledWith(
342
+ "feishu-p2p",
343
+ "Codex Usage",
344
+ expect.stringContaining("[██░░░░░░░░░░░░░░░░░░]"),
345
+ "blue",
346
+ );
347
+ });
348
+
349
+ it("handles /usage as Cursor usage in Cursor chats", async () => {
350
+ const platform = mockPlatform("feishu");
351
+ await recordSessionRegistry({
352
+ chatId: "cursor-chat",
353
+ sessionId: "sid-cursor",
354
+ tool: "cursor",
355
+ chatName: "cursor-session",
356
+ running: false,
357
+ });
358
+
359
+ await handleCommand(platform, "/usage", "cursor-chat", "ou-user", Date.now(), "group");
360
+
361
+ expect(platform.createGroup).not.toHaveBeenCalled();
362
+ expect(mockGetCodexUsageSummary).not.toHaveBeenCalled();
363
+ expect(mockGetCursorUsageSummary).toHaveBeenCalled();
364
+ expect(platform.sendCard).toHaveBeenCalledWith(
365
+ "cursor-chat",
366
+ "Cursor Usage",
367
+ expect.stringContaining("Individual used: $1012.52"),
368
+ "blue",
369
+ );
370
+ expect(platform.sendCard).toHaveBeenCalledWith(
371
+ "cursor-chat",
372
+ "Cursor Usage",
373
+ expect.stringContaining("Pool remaining: $171417.76"),
374
+ "blue",
375
+ );
376
+ });
377
+
378
+ it("advertises /usage in new Codex and Cursor session ready messages", async () => {
379
+ const codexPlatform = mockPlatform("feishu");
380
+ _setAdapterForToolForTest("codex", mockAdapter("sid-codex"));
381
+
382
+ await handleCommand(codexPlatform, "/new codex", "feishu-p2p", "ou-user", Date.now(), "p2p");
383
+
384
+ expect(codexPlatform.sendCard).toHaveBeenCalledWith(
385
+ "feishu-group",
386
+ "Codex Session Ready",
387
+ expect.stringContaining("发送 **/usage** 查看 Codex 5h 和周用量。"),
388
+ "green",
389
+ );
390
+
391
+ const cursorPlatform = mockPlatform("feishu");
392
+ _setAdapterForToolForTest("cursor", mockAdapter("sid-cursor"));
393
+
394
+ await handleCommand(cursorPlatform, "/new cursor", "feishu-p2p-cursor", "ou-user", Date.now(), "p2p");
395
+
396
+ const cursorReadyCall = vi.mocked(cursorPlatform.sendCard).mock.calls.find(
397
+ ([chatId, title]) => chatId === "feishu-group" && title === "Cursor Session Ready",
398
+ );
399
+ expect(cursorReadyCall?.[2]).toContain("/usage");
400
+
401
+ const claudePlatform = mockPlatform("feishu");
402
+ await handleCommand(claudePlatform, "/new claude", "feishu-p2p-2", "ou-user", Date.now(), "p2p");
403
+
404
+ const claudeReadyCall = vi.mocked(claudePlatform.sendCard).mock.calls.find(
405
+ ([chatId, title]) => chatId === "feishu-group" && title === "Claude Code Session Ready",
406
+ );
407
+ expect(claudeReadyCall?.[2]).not.toContain("/usage");
408
+ });
409
+ });
package/src/cards.ts CHANGED
@@ -133,11 +133,11 @@ export function buildHelpCard(
133
133
  "发送 **/new cursor** 创建新 Cursor 会话",
134
134
  "发送 **/new codex** 创建新 Codex 会话",
135
135
  "发送 **/newh** 重置当前会话(沿用当前工作目录,不切换)",
136
- "发送 **/plan** 以规划模式提问(只读,不执行写操作)",
137
- "发送 **/ask** 以问答模式提问(只读,不执行写操作)",
138
- "发送 **/usage** 查看 Codex 5h 和周用量",
139
- "发送 **/restart** 重启 ChatCCC 进程",
140
- "发送 **/updateg** 更新并重启(仅 npm 全局安装可用)",
136
+ "发送 **/plan** 以规划模式提问(只读,不执行写操作)",
137
+ "发送 **/ask** 以问答模式提问(只读,不执行写操作)",
138
+ "发送 **/usage** 查看 Codex 5h 和周用量",
139
+ "发送 **/restart** 重启 ChatCCC 进程",
140
+ "发送 **/update** 更新并重启(仅 npm 全局安装可用)",
141
141
  ].join("\n");
142
142
  return JSON.stringify({
143
143
  config: { wide_screen_mode: true },
@@ -151,7 +151,7 @@ export function buildHelpCard(
151
151
  { text: "新建 Cursor 会话(/new cursor)", value: JSON.stringify({ cmd: "new cursor" }), type: "primary" },
152
152
  { text: "新建 Codex 会话(/new codex)", value: JSON.stringify({ cmd: "new codex" }), type: "primary" },
153
153
  { text: "重启 ChatCCC(/restart)", value: JSON.stringify({ cmd: "restart" }), type: "danger" },
154
- { text: "更新并重启(/updateg)", value: JSON.stringify({ cmd: "updateg" }), type: "danger" },
154
+ { text: "更新并重启(/update)", value: JSON.stringify({ cmd: "update" }), type: "danger" },
155
155
  { text: "切换工作路径(/cd)", value: JSON.stringify({ cmd: "cd" }), type: "default" },
156
156
  ]),
157
157
  ],
package/src/config.ts CHANGED
@@ -81,6 +81,8 @@ export interface CursorConfig {
81
81
  /** Cursor Agent CLI 可执行文件绝对路径;留空时由运行时按 LocalAppData / PATH 兜底 */
82
82
  path: string;
83
83
  model: string;
84
+ avatarBatteryMode: CursorAvatarBatteryMode;
85
+ onDemandMonthlyBudget: number;
84
86
  }
85
87
 
86
88
  export interface CodexConfig {
@@ -125,6 +127,7 @@ export interface AppConfig {
125
127
 
126
128
  export type AgentTool = "claude" | "cursor" | "codex";
127
129
  export const AGENT_TOOLS: AgentTool[] = ["claude", "cursor", "codex"];
130
+ export type CursorAvatarBatteryMode = "apiPercent" | "onDemandUse";
128
131
 
129
132
  /** 获取指定 agent 配置中所有模型相关的值(最多 100 个,去重) */
130
133
  export function getAllModelsForTool(tool: AgentTool, cfg: AppConfig = config): string[] {
@@ -326,6 +329,15 @@ function normalizePlatformType(raw: string): "feishu" | "lark" {
326
329
  return "feishu";
327
330
  }
328
331
 
332
+ function normalizeCursorAvatarBatteryMode(raw: unknown): CursorAvatarBatteryMode {
333
+ return raw === "onDemandUse" ? "onDemandUse" : "apiPercent";
334
+ }
335
+
336
+ function normalizeCursorOnDemandMonthlyBudget(raw: unknown): number {
337
+ const value = typeof raw === "string" ? Number(raw.trim()) : Number(raw);
338
+ return Number.isFinite(value) && value > 0 ? value : 1000;
339
+ }
340
+
329
341
  function loadConfig(): AppConfig {
330
342
  const defaults: AppConfig = {
331
343
  feishu: { appId: "", appSecret: "" },
@@ -334,7 +346,14 @@ function loadConfig(): AppConfig {
334
346
  gitTimeoutSeconds: 180,
335
347
  allowInterrupt: false,
336
348
  claude: { enabled: false, defaultAgent: true, model: "", subagentModel: "", effort: "", apiKey: "", baseUrl: "", maxTurn: 0 },
337
- cursor: { enabled: false, defaultAgent: false, path: "", model: "claude-opus-4-7-max" },
349
+ cursor: {
350
+ enabled: false,
351
+ defaultAgent: false,
352
+ path: "",
353
+ model: "claude-opus-4-7-max",
354
+ avatarBatteryMode: "apiPercent",
355
+ onDemandMonthlyBudget: 1000,
356
+ },
338
357
  codex: { enabled: false, defaultAgent: false, path: "", model: "", effort: "" },
339
358
  };
340
359
 
@@ -377,7 +396,15 @@ function loadConfig(): AppConfig {
377
396
 
378
397
  let parsed: Partial<AppConfig> & {
379
398
  claude?: Partial<ClaudeConfig> & { enabled?: unknown };
380
- cursor?: { enabled?: unknown; defaultAgent?: unknown; path?: unknown; command?: unknown; model?: unknown };
399
+ cursor?: {
400
+ enabled?: unknown;
401
+ defaultAgent?: unknown;
402
+ path?: unknown;
403
+ command?: unknown;
404
+ model?: unknown;
405
+ avatarBatteryMode?: unknown;
406
+ onDemandMonthlyBudget?: unknown;
407
+ };
381
408
  codex?: { enabled?: unknown; defaultAgent?: unknown; path?: unknown; command?: unknown; model?: unknown; effort?: unknown };
382
409
  };
383
410
  try {
@@ -491,6 +518,8 @@ function loadConfig(): AppConfig {
491
518
  defaultAgent: defaultTool === "cursor",
492
519
  path: readToolCliPath(cursorRaw, { label: "cursor", onLegacyField }),
493
520
  model: normalizeOptionalConfigField(cursorRaw.model, { label: "cursor.model", fallback: "claude-opus-4-7-max" }),
521
+ avatarBatteryMode: normalizeCursorAvatarBatteryMode(cursorRaw.avatarBatteryMode),
522
+ onDemandMonthlyBudget: normalizeCursorOnDemandMonthlyBudget(cursorRaw.onDemandMonthlyBudget),
494
523
  },
495
524
  codex: {
496
525
  enabled: codexEnabled,
@@ -0,0 +1,128 @@
1
+ import { existsSync } from "node:fs";
2
+ import { createRequire } from "node:module";
3
+ import { homedir } from "node:os";
4
+ import { join } from "node:path";
5
+
6
+ const require = createRequire(import.meta.url);
7
+
8
+ const CURSOR_USAGE_URL = "https://api2.cursor.sh/aiserver.v1.DashboardService/GetCurrentPeriodUsage";
9
+
10
+ export type CursorUsageSummary = {
11
+ billingCycleStart?: string;
12
+ billingCycleEnd?: string;
13
+ planUsage?: {
14
+ totalSpend?: number;
15
+ includedSpend?: number;
16
+ bonusSpend?: number;
17
+ limit?: number;
18
+ remainingBonus?: boolean;
19
+ bonusTooltip?: string;
20
+ autoPercentUsed?: number;
21
+ apiPercentUsed?: number;
22
+ totalPercentUsed?: number;
23
+ };
24
+ spendLimitUsage?: {
25
+ totalSpend?: number;
26
+ pooledLimit?: number;
27
+ pooledUsed?: number;
28
+ pooledRemaining?: number;
29
+ individualUsed?: number;
30
+ limitType?: string;
31
+ };
32
+ displayThreshold?: number;
33
+ enabled?: boolean;
34
+ displayMessage?: string;
35
+ autoModelSelectedDisplayMessage?: string;
36
+ namedModelSelectedDisplayMessage?: string;
37
+ autoBucketModels?: string[];
38
+ };
39
+
40
+ type SqliteStatement = {
41
+ get(value?: unknown): unknown;
42
+ all(value?: unknown): unknown[];
43
+ };
44
+
45
+ type SqliteDatabase = {
46
+ prepare(sql: string): SqliteStatement;
47
+ close(): void;
48
+ };
49
+
50
+ function getCursorDbPath(): string {
51
+ if (process.platform === "win32") {
52
+ return join(process.env.APPDATA || "", "Cursor", "User", "globalStorage", "state.vscdb");
53
+ }
54
+
55
+ if (process.platform === "darwin") {
56
+ return join(homedir(), "Library", "Application Support", "Cursor", "User", "globalStorage", "state.vscdb");
57
+ }
58
+
59
+ return join(homedir(), ".config", "Cursor", "User", "globalStorage", "state.vscdb");
60
+ }
61
+
62
+ function openSqliteDatabase(dbPath: string): SqliteDatabase {
63
+ try {
64
+ const { DatabaseSync } = require("node:sqlite") as {
65
+ DatabaseSync?: new (path: string, opts: { readOnly: boolean }) => SqliteDatabase;
66
+ };
67
+ if (DatabaseSync) return new DatabaseSync(dbPath, { readOnly: true });
68
+ } catch (error) {
69
+ const code = (error as { code?: string }).code;
70
+ if (code !== "ERR_UNKNOWN_BUILTIN_MODULE" && code !== "MODULE_NOT_FOUND") {
71
+ throw error;
72
+ }
73
+ }
74
+
75
+ try {
76
+ const Database = require("better-sqlite3") as new (path: string, opts: { readonly: boolean }) => SqliteDatabase;
77
+ return new Database(dbPath, { readonly: true });
78
+ } catch {
79
+ throw new Error("node:sqlite is unavailable and better-sqlite3 is not installed");
80
+ }
81
+ }
82
+
83
+ function parseStorageValue(value: unknown): unknown {
84
+ if (typeof value !== "string") return value;
85
+ try {
86
+ return JSON.parse(value);
87
+ } catch {
88
+ return value;
89
+ }
90
+ }
91
+
92
+ function getCursorAccessToken(dbPath = getCursorDbPath()): string {
93
+ if (!existsSync(dbPath)) {
94
+ throw new Error(`Cursor database not found: ${dbPath}`);
95
+ }
96
+
97
+ const db = openSqliteDatabase(dbPath);
98
+
99
+ try {
100
+ const row = db.prepare("SELECT value FROM ItemTable WHERE key = ?").get("cursorAuth/accessToken") as
101
+ | { value?: unknown }
102
+ | undefined;
103
+ const token = parseStorageValue(row?.value);
104
+ if (typeof token === "string" && token.length > 0) return token;
105
+ throw new Error("Cursor access token not found");
106
+ } finally {
107
+ db.close();
108
+ }
109
+ }
110
+
111
+ export async function getCursorUsageSummary(): Promise<CursorUsageSummary> {
112
+ const token = getCursorAccessToken();
113
+ const response = await fetch(CURSOR_USAGE_URL, {
114
+ method: "POST",
115
+ headers: {
116
+ Authorization: `Bearer ${token}`,
117
+ "Content-Type": "application/json",
118
+ "Connect-Protocol-Version": "1",
119
+ },
120
+ body: "{}",
121
+ });
122
+
123
+ if (!response.ok) {
124
+ throw new Error(`Cursor usage API returned ${response.status} ${response.statusText}`);
125
+ }
126
+
127
+ return (await response.json()) as CursorUsageSummary;
128
+ }