chatccc 0.2.178 → 0.2.180

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.
@@ -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
+ }
package/src/feishu-api.ts CHANGED
@@ -17,7 +17,9 @@ import {
17
17
  ts,
18
18
  resolveDefaultAgentTool,
19
19
  toolDisplayName,
20
+ config,
20
21
  } from "./config.ts";
22
+ import { getCursorUsageSummary, type CursorUsageSummary } from "./cursor-usage.ts";
21
23
  import { applyPrivacy } from "./privacy.ts";
22
24
 
23
25
  // ---------------------------------------------------------------------------
@@ -316,6 +318,7 @@ const AVATAR_SIZE = 256;
316
318
  const AVATAR_BADGE_SIZE = 92;
317
319
  const AVATAR_BADGE_MARGIN = 10;
318
320
  const CODEX_AVATAR_USAGE_STYLE_VERSION = "usage-ring-gray-consumed-v13";
321
+ const CURSOR_AVATAR_USAGE_STYLE_VERSION = "usage-battery-v1";
319
322
 
320
323
  export interface CodexUsageBalance {
321
324
  usedPercent: number;
@@ -344,7 +347,12 @@ function avatarCombinationPath(tool: string, status: string): string {
344
347
  return resolvePath(AVATAR_COMBINATIONS_DIR, `avatar_${normalizeAvatarTool(tool)}_${normalizeAvatarStatus(status)}.png`);
345
348
  }
346
349
 
347
- function avatarCacheKey(tool: string, status: string, codexUsage: CodexUsageSummary | null = null): string {
350
+ function avatarCacheKey(
351
+ tool: string,
352
+ status: string,
353
+ codexUsage: CodexUsageSummary | null = null,
354
+ cursorBatteryPercent: number | null = null,
355
+ ): string {
348
356
  const normalizedTool = normalizeAvatarTool(tool);
349
357
  const normalizedStatus = normalizeAvatarStatus(status);
350
358
  if (normalizedTool === "codex") {
@@ -352,6 +360,11 @@ function avatarCacheKey(tool: string, status: string, codexUsage: CodexUsageSumm
352
360
  ? `${normalizedTool}:${normalizedStatus}:${CODEX_AVATAR_USAGE_STYLE_VERSION}:week-battery:${codexUsage.weekly?.remainingPercent}:5h-ring:${codexUsage.fiveHour.remainingPercent}`
353
361
  : `${normalizedTool}:${normalizedStatus}:plain`;
354
362
  }
363
+ if (normalizedTool === "cursor") {
364
+ return cursorBatteryPercent !== null
365
+ ? `${normalizedTool}:${normalizedStatus}:${CURSOR_AVATAR_USAGE_STYLE_VERSION}:battery:${cursorBatteryPercent}`
366
+ : `${normalizedTool}:${normalizedStatus}:plain`;
367
+ }
355
368
  return `${normalizedTool}:${normalizedStatus}`;
356
369
  }
357
370
 
@@ -460,6 +473,28 @@ async function fetchCodexAvatarUsage(): Promise<CodexUsageSummary | null> {
460
473
  }
461
474
  }
462
475
 
476
+ function cursorAvatarBatteryPercentFromUsage(summary: CursorUsageSummary): number {
477
+ if (config.cursor.avatarBatteryMode === "onDemandUse") {
478
+ const usedCents = Number(summary.spendLimitUsage?.individualUsed);
479
+ if (!Number.isFinite(usedCents)) throw new Error("missing spendLimitUsage.individualUsed");
480
+ const budget = config.cursor.onDemandMonthlyBudget > 0 ? config.cursor.onDemandMonthlyBudget : 1000;
481
+ return clampPercent(100 - ((usedCents / 100) / budget) * 100);
482
+ }
483
+
484
+ const apiPercentUsed = Number(summary.planUsage?.apiPercentUsed);
485
+ if (!Number.isFinite(apiPercentUsed)) throw new Error("missing planUsage.apiPercentUsed");
486
+ return clampPercent(100 - apiPercentUsed);
487
+ }
488
+
489
+ async function fetchCursorAvatarBatteryPercent(): Promise<number | null> {
490
+ try {
491
+ return cursorAvatarBatteryPercentFromUsage(await getCursorUsageSummary());
492
+ } catch (err) {
493
+ console.warn(`[${ts()}] [AVATAR] Cursor usage unavailable, using plain avatar: ${(err as Error).message}`);
494
+ return null;
495
+ }
496
+ }
497
+
463
498
  function codexUsagePalette(remainingPercent: number): { start: string; end: string; glow: string } {
464
499
  if (remainingPercent <= 25) return { start: "#ef4444", end: "#fb923c", glow: "#fed7aa" };
465
500
  if (remainingPercent <= 60) return { start: "#eab308", end: "#facc15", glow: "#fef3c7" };
@@ -573,20 +608,32 @@ async function buildAgentBadgeOverlay(tool: string): Promise<sharp.OverlayOption
573
608
  };
574
609
  }
575
610
 
576
- async function renderAvatar(tool: string, status: string, codexUsage: CodexUsageSummary | null = null): Promise<{ buffer: Buffer; contentType: string; filename: string }> {
611
+ async function renderAvatar(
612
+ tool: string,
613
+ status: string,
614
+ codexUsage: CodexUsageSummary | null = null,
615
+ cursorBatteryPercent: number | null = null,
616
+ ): Promise<{ buffer: Buffer; contentType: string; filename: string }> {
577
617
  const normalizedTool = normalizeAvatarTool(tool);
578
618
  const normalizedStatus = normalizeAvatarStatus(status);
579
619
  const composites: sharp.OverlayOptions[] = [];
580
620
 
581
- const useDynamicCodexAvatar = normalizedTool === "codex" && codexUsage?.weekly;
582
- const basePath = useDynamicCodexAvatar
621
+ const codexWeeklyUsage = normalizedTool === "codex" ? codexUsage?.weekly : null;
622
+ const useDynamicCodexAvatar = codexUsage && codexWeeklyUsage;
623
+ const useDynamicCursorAvatar = normalizedTool === "cursor" && cursorBatteryPercent !== null;
624
+ const basePath = useDynamicCodexAvatar || useDynamicCursorAvatar
583
625
  ? AVATAR_SOURCES[normalizedStatus]
584
626
  : avatarCombinationPath(normalizedTool, normalizedStatus);
585
627
 
586
628
  if (useDynamicCodexAvatar) {
587
629
  composites.push(
588
630
  { input: buildCodexUsageRingSvg(codexUsage.fiveHour.remainingPercent), left: 0, top: 0 },
589
- { input: buildCodexUsageBatterySvg(codexUsage.weekly.remainingPercent), left: 0, top: 0 },
631
+ { input: buildCodexUsageBatterySvg(codexWeeklyUsage.remainingPercent), left: 0, top: 0 },
632
+ await buildAgentBadgeOverlay(normalizedTool),
633
+ );
634
+ } else if (useDynamicCursorAvatar) {
635
+ composites.push(
636
+ { input: buildCodexUsageBatterySvg(cursorBatteryPercent), left: 0, top: 0 },
590
637
  await buildAgentBadgeOverlay(normalizedTool),
591
638
  );
592
639
  }
@@ -608,12 +655,20 @@ async function renderAvatar(tool: string, status: string, codexUsage: CodexUsage
608
655
  contentType: "image/jpeg",
609
656
  filename: codexUsage?.weekly
610
657
  ? `avatar_${normalizedTool}_${normalizedStatus}_week_${codexUsage.weekly.remainingPercent}_5h_${codexUsage.fiveHour.remainingPercent}.jpg`
658
+ : cursorBatteryPercent !== null
659
+ ? `avatar_${normalizedTool}_${normalizedStatus}_battery_${cursorBatteryPercent}.jpg`
611
660
  : `avatar_${normalizedTool}_${normalizedStatus}.jpg`,
612
661
  };
613
662
  }
614
663
 
615
- async function uploadImage(token: string, tool: string, status: string, codexUsage: CodexUsageSummary | null = null): Promise<string> {
616
- const image = await renderAvatar(tool, status, codexUsage);
664
+ async function uploadImage(
665
+ token: string,
666
+ tool: string,
667
+ status: string,
668
+ codexUsage: CodexUsageSummary | null = null,
669
+ cursorBatteryPercent: number | null = null,
670
+ ): Promise<string> {
671
+ const image = await renderAvatar(tool, status, codexUsage, cursorBatteryPercent);
617
672
  const blob = new Blob([new Uint8Array(image.buffer)], { type: image.contentType });
618
673
  const form = new FormData();
619
674
  form.append("image_type", "avatar");
@@ -639,10 +694,11 @@ async function getOrUploadAvatarKey(token: string, tool: string, status: string)
639
694
  const normalizedTool = normalizeAvatarTool(tool);
640
695
  const normalizedStatus = normalizeAvatarStatus(status);
641
696
  const codexUsage = normalizedTool === "codex" ? await fetchCodexAvatarUsage() : null;
642
- const keyName = avatarCacheKey(normalizedTool, normalizedStatus, codexUsage);
697
+ const cursorBatteryPercent = normalizedTool === "cursor" ? await fetchCursorAvatarBatteryPercent() : null;
698
+ const keyName = avatarCacheKey(normalizedTool, normalizedStatus, codexUsage, cursorBatteryPercent);
643
699
  const cached = avatarKeyCache.get(keyName);
644
700
  if (cached) return cached;
645
- const key = await uploadImage(token, normalizedTool, normalizedStatus, codexUsage);
701
+ const key = await uploadImage(token, normalizedTool, normalizedStatus, codexUsage, cursorBatteryPercent);
646
702
  avatarKeyCache.set(keyName, key);
647
703
  await persistAvatarKeyCache().catch((err) => {
648
704
  console.error(`[${ts()}] [AVATAR] persist cache FAIL: ${(err as Error).message}`);
package/src/index.ts CHANGED
@@ -213,7 +213,7 @@ function parseCardAction(data: unknown): CardActionResult | null {
213
213
  }
214
214
  if (!cmd) return null;
215
215
 
216
- const CMD_MAP: Record<string, string> = { stop: "/stop", cancel: "/cancel", new: "/new", "new claude": "/new claude", "new cursor": "/new cursor", "new codex": "/new codex", restart: "/restart", updateg: "/updateg", state: "/state", cd: "/cd", sessions: "/sessions", newh: "/newh" };
216
+ const CMD_MAP: Record<string, string> = { stop: "/stop", cancel: "/cancel", new: "/new", "new claude": "/new claude", "new cursor": "/new cursor", "new codex": "/new codex", restart: "/restart", update: "/update", state: "/state", cd: "/cd", sessions: "/sessions", newh: "/newh" };
217
217
  let text = CMD_MAP[cmd] ?? "";
218
218
  if (cmd === "cd" && typeof action.value === "object" && action.value !== null) {
219
219
  const path = (action.value as Record<string, string>).path;