chatccc 0.2.186 → 0.2.188

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/src/feishu-api.ts CHANGED
@@ -20,6 +20,7 @@ import {
20
20
  config,
21
21
  } from "./config.ts";
22
22
  import { getCursorUsageSummary, type CursorUsageSummary } from "./cursor-usage.ts";
23
+ import type { ChatAvatarUsageHints } from "./platform-adapter.ts";
23
24
  import { applyPrivacy } from "./privacy.ts";
24
25
 
25
26
  // ---------------------------------------------------------------------------
@@ -302,10 +303,10 @@ const AVATAR_DIR = resolvePath(PROJECT_ROOT, "images", "avatars");
302
303
  const AVATAR_BADGE_DIR = resolvePath(AVATAR_DIR, "badges");
303
304
  const AVATAR_COMBINATIONS_DIR = resolvePath(AVATAR_DIR, "combinations");
304
305
  const AVATAR_KEY_CACHE_FILE = resolvePath(USER_DATA_DIR, "state", "avatar-image-keys.json");
305
- const CODEX_AUTH_FILE = resolvePath(homedir(), ".codex", "auth.json");
306
- const CODEX_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
307
- const CODEX_RESET_CREDITS_URL = "https://chatgpt.com/backend-api/wham/rate-limit-reset-credits";
308
- const CODEX_RESET_CONSUME_URL = "https://chatgpt.com/backend-api/wham/rate-limit-reset-credits/consume";
306
+ const CODEX_AUTH_FILE = resolvePath(homedir(), ".codex", "auth.json");
307
+ const CODEX_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
308
+ const CODEX_RESET_CREDITS_URL = "https://chatgpt.com/backend-api/wham/rate-limit-reset-credits";
309
+ const CODEX_RESET_CONSUME_URL = "https://chatgpt.com/backend-api/wham/rate-limit-reset-credits/consume";
309
310
  const AVATAR_SOURCES: Record<string, string> = {
310
311
  new: resolvePath(AVATAR_DIR, "status_new.png"),
311
312
  busy: resolvePath(AVATAR_DIR, "status_busy.png"),
@@ -322,31 +323,31 @@ const AVATAR_BADGE_MARGIN = 10;
322
323
  const CODEX_AVATAR_USAGE_STYLE_VERSION = "usage-ring-gray-consumed-v13";
323
324
  const CURSOR_AVATAR_USAGE_STYLE_VERSION = "usage-battery-v1";
324
325
 
325
- export interface CodexUsageBalance {
326
- usedPercent: number;
327
- remainingPercent: number;
328
- resetAtEpochSeconds: number | null;
329
- resetAfterSeconds: number | null;
330
- }
331
-
332
- export interface CodexRateLimitResetCredit {
333
- grantedAt: string | null;
334
- expiresAt: string;
335
- }
336
-
337
- export interface CodexUsageSummary {
338
- fiveHour: CodexUsageBalance;
339
- weekly: CodexUsageBalance | null;
340
- rateLimitResetCreditsAvailable: number | null;
341
- rateLimitResetCredits: CodexRateLimitResetCredit[] | null;
342
- }
343
-
344
- export type CodexResetConsumeCode = "reset" | "nothing_to_reset" | "no_credit" | "already_redeemed";
345
-
346
- export interface CodexResetConsumeResult {
347
- code: CodexResetConsumeCode;
348
- windowsReset: number;
349
- }
326
+ export interface CodexUsageBalance {
327
+ usedPercent: number;
328
+ remainingPercent: number;
329
+ resetAtEpochSeconds: number | null;
330
+ resetAfterSeconds: number | null;
331
+ }
332
+
333
+ export interface CodexRateLimitResetCredit {
334
+ grantedAt: string | null;
335
+ expiresAt: string;
336
+ }
337
+
338
+ export interface CodexUsageSummary {
339
+ fiveHour: CodexUsageBalance;
340
+ weekly: CodexUsageBalance | null;
341
+ rateLimitResetCreditsAvailable: number | null;
342
+ rateLimitResetCredits: CodexRateLimitResetCredit[] | null;
343
+ }
344
+
345
+ export type CodexResetConsumeCode = "reset" | "nothing_to_reset" | "no_credit" | "already_redeemed";
346
+
347
+ export interface CodexResetConsumeResult {
348
+ code: CodexResetConsumeCode;
349
+ windowsReset: number;
350
+ }
350
351
 
351
352
  const avatarKeyCache = new Map<string, string>();
352
353
  let avatarKeyCacheLoaded = false;
@@ -427,167 +428,172 @@ function usageBalanceFromWindow(raw: Record<string, unknown>, fieldName: string)
427
428
  };
428
429
  }
429
430
 
430
- function parseOptionalUsageWindow(rateLimit: Record<string, unknown>, keys: string[]): CodexUsageBalance | null {
431
+ function parseOptionalUsageWindow(rateLimit: Record<string, unknown>, keys: string[]): CodexUsageBalance | null {
431
432
  for (const key of keys) {
432
433
  const raw = rateLimit[key];
433
434
  if (!raw || typeof raw !== "object") continue;
434
435
  if ((raw as { used_percent?: unknown }).used_percent === undefined) continue;
435
436
  return usageBalanceFromWindow(raw as Record<string, unknown>, `rate_limit.${key}`);
436
437
  }
437
- return null;
438
- }
439
-
440
- function parseRateLimitResetCredits(data: Record<string, unknown>): number | null {
441
- const raw = data.rate_limit_reset_credits;
442
- if (!raw || typeof raw !== "object") return null;
443
- const availableCount = Number((raw as { available_count?: unknown }).available_count);
444
- if (!Number.isFinite(availableCount)) return null;
445
- return Math.max(0, Math.trunc(availableCount));
446
- }
447
-
448
- interface CodexAuth {
449
- accessToken: string;
450
- accountId?: string;
451
- }
452
-
453
- async function getCodexAuth(): Promise<CodexAuth | null> {
454
- try {
455
- const raw = await readFile(CODEX_AUTH_FILE, "utf-8");
456
- const parsed = JSON.parse(raw) as { tokens?: { access_token?: unknown; account_id?: unknown } };
457
- const token = parsed.tokens?.access_token;
458
- if (typeof token !== "string" || !token.trim()) return null;
459
- const accountId = parsed.tokens?.account_id;
460
- return {
461
- accessToken: token,
462
- accountId: typeof accountId === "string" && accountId.trim() ? accountId : undefined,
463
- };
464
- } catch {
465
- return null;
466
- }
467
- }
468
-
469
- async function getCodexAccessToken(): Promise<string | null> {
470
- return (await getCodexAuth())?.accessToken ?? null;
471
- }
472
-
473
- function codexAuthHeaders(auth: CodexAuth): Record<string, string> {
474
- const headers: Record<string, string> = {
475
- Authorization: `Bearer ${auth.accessToken}`,
476
- "OpenAI-Beta": "codex-1",
477
- originator: "Codex Desktop",
478
- };
479
- if (auth.accountId) headers["ChatGPT-Account-ID"] = auth.accountId;
480
- return headers;
481
- }
482
-
483
- function parseCodexResetCreditDetails(data: Record<string, unknown>): {
484
- availableCount: number | null;
485
- availableCredits: CodexRateLimitResetCredit[];
486
- } {
487
- const availableCount = Number(data.available_count);
488
- const credits = Array.isArray(data.credits) ? data.credits : [];
489
- return {
490
- availableCount: Number.isFinite(availableCount) ? Math.max(0, Math.trunc(availableCount)) : null,
491
- availableCredits: credits.flatMap((raw) => {
492
- if (!raw || typeof raw !== "object") return [];
493
- const credit = raw as { status?: unknown; granted_at?: unknown; expires_at?: unknown };
494
- if (credit.status !== "available") return [];
495
- if (typeof credit.expires_at !== "string" || !credit.expires_at.trim()) return [];
496
- return [{
497
- grantedAt: typeof credit.granted_at === "string" && credit.granted_at.trim() ? credit.granted_at : null,
498
- expiresAt: credit.expires_at,
499
- }];
500
- }),
501
- };
502
- }
503
-
504
- async function fetchCodexRateLimitResetCredits(auth: CodexAuth): Promise<{
505
- availableCount: number | null;
506
- availableCredits: CodexRateLimitResetCredit[];
507
- } | null> {
508
- try {
509
- const resp = await fetch(CODEX_RESET_CREDITS_URL, {
510
- headers: codexAuthHeaders(auth),
511
- });
512
- const text = await resp.text();
513
- if (!resp.ok) throw new Error(`HTTP ${resp.status}: ${text.slice(0, 160)}`);
514
- return parseCodexResetCreditDetails(JSON.parse(text) as Record<string, unknown>);
515
- } catch (err) {
516
- console.warn(`[Codex] reset credits lookup failed: ${(err as Error).message}`);
517
- return null;
518
- }
519
- }
520
-
521
- export async function getCodexUsageSummary(): Promise<CodexUsageSummary> {
522
- const auth = await getCodexAuth();
523
- if (!auth) throw new Error("missing ~/.codex/auth.json access token");
524
-
525
- const resetCreditsPromise = fetchCodexRateLimitResetCredits(auth);
526
-
527
- const resp = await fetch(CODEX_USAGE_URL, {
528
- headers: { Authorization: `Bearer ${auth.accessToken}` },
529
- });
530
- const text = await resp.text();
531
- if (!resp.ok) throw new Error(`HTTP ${resp.status}: ${text.slice(0, 160)}`);
532
-
533
- const data = JSON.parse(text) as {
534
- rate_limit?: Record<string, unknown>;
535
- rate_limit_reset_credits?: unknown;
536
- };
537
- const rateLimit = data.rate_limit;
538
- if (!rateLimit || typeof rateLimit !== "object") throw new Error("missing rate_limit");
539
-
540
- const fiveHour = parseOptionalUsageWindow(rateLimit, ["primary_window"]);
541
- if (!fiveHour) throw new Error("missing rate_limit.primary_window.used_percent");
542
- const resetCredits = await resetCreditsPromise;
543
-
544
- return {
545
- fiveHour,
546
- weekly: parseOptionalUsageWindow(rateLimit, [
547
- "secondary_window",
548
- "weekly_window",
549
- "week_window",
550
- "long_window",
551
- ]),
552
- rateLimitResetCreditsAvailable: resetCredits?.availableCount ?? parseRateLimitResetCredits(data),
553
- rateLimitResetCredits: resetCredits?.availableCredits ?? null,
554
- };
555
- }
556
-
557
- export async function consumeCodexRateLimitResetCredit(redeemRequestId: string): Promise<CodexResetConsumeResult> {
558
- if (!redeemRequestId.trim()) throw new Error("missing redeem_request_id");
559
- const token = await getCodexAccessToken();
560
- if (!token) throw new Error("missing ~/.codex/auth.json access token");
561
-
562
- const resp = await fetch(CODEX_RESET_CONSUME_URL, {
563
- method: "POST",
564
- headers: {
565
- Authorization: `Bearer ${token}`,
566
- "Content-Type": "application/json",
567
- },
568
- body: JSON.stringify({ redeem_request_id: redeemRequestId }),
569
- });
570
- const text = await resp.text();
571
- if (!resp.ok) throw new Error(`HTTP ${resp.status}: ${text.slice(0, 160)}`);
572
-
573
- const data = JSON.parse(text) as { code?: unknown; windows_reset?: unknown };
574
- const code = data.code;
575
- if (
576
- code !== "reset"
577
- && code !== "nothing_to_reset"
578
- && code !== "no_credit"
579
- && code !== "already_redeemed"
580
- ) {
581
- throw new Error("missing or unknown reset result code");
582
- }
583
- const windowsReset = Number(data.windows_reset);
584
- return {
585
- code,
586
- windowsReset: Number.isFinite(windowsReset) ? Math.max(0, Math.trunc(windowsReset)) : 0,
587
- };
588
- }
589
-
590
- async function fetchCodexAvatarUsage(): Promise<CodexUsageSummary | null> {
438
+ return null;
439
+ }
440
+
441
+ function parseRateLimitResetCredits(data: Record<string, unknown>): number | null {
442
+ const raw = data.rate_limit_reset_credits;
443
+ if (!raw || typeof raw !== "object") return null;
444
+ const availableCount = Number((raw as { available_count?: unknown }).available_count);
445
+ if (!Number.isFinite(availableCount)) return null;
446
+ return Math.max(0, Math.trunc(availableCount));
447
+ }
448
+
449
+ interface CodexAuth {
450
+ accessToken: string;
451
+ accountId?: string;
452
+ }
453
+
454
+ async function getCodexAuth(): Promise<CodexAuth | null> {
455
+ try {
456
+ const raw = await readFile(CODEX_AUTH_FILE, "utf-8");
457
+ const parsed = JSON.parse(raw) as { tokens?: { access_token?: unknown; account_id?: unknown } };
458
+ const token = parsed.tokens?.access_token;
459
+ if (typeof token !== "string" || !token.trim()) return null;
460
+ const accountId = parsed.tokens?.account_id;
461
+ return {
462
+ accessToken: token,
463
+ accountId: typeof accountId === "string" && accountId.trim() ? accountId : undefined,
464
+ };
465
+ } catch {
466
+ return null;
467
+ }
468
+ }
469
+
470
+ async function getCodexAccessToken(): Promise<string | null> {
471
+ return (await getCodexAuth())?.accessToken ?? null;
472
+ }
473
+
474
+ function codexAuthHeaders(auth: CodexAuth): Record<string, string> {
475
+ const headers: Record<string, string> = {
476
+ Authorization: `Bearer ${auth.accessToken}`,
477
+ "OpenAI-Beta": "codex-1",
478
+ originator: "Codex Desktop",
479
+ };
480
+ if (auth.accountId) headers["ChatGPT-Account-ID"] = auth.accountId;
481
+ return headers;
482
+ }
483
+
484
+ function parseCodexResetCreditDetails(data: Record<string, unknown>): {
485
+ availableCount: number | null;
486
+ availableCredits: CodexRateLimitResetCredit[];
487
+ } {
488
+ const availableCount = Number(data.available_count);
489
+ const credits = Array.isArray(data.credits) ? data.credits : [];
490
+ return {
491
+ availableCount: Number.isFinite(availableCount) ? Math.max(0, Math.trunc(availableCount)) : null,
492
+ availableCredits: credits.flatMap((raw) => {
493
+ if (!raw || typeof raw !== "object") return [];
494
+ const credit = raw as { status?: unknown; granted_at?: unknown; expires_at?: unknown };
495
+ if (credit.status !== "available") return [];
496
+ if (typeof credit.expires_at !== "string" || !credit.expires_at.trim()) return [];
497
+ return [{
498
+ grantedAt: typeof credit.granted_at === "string" && credit.granted_at.trim() ? credit.granted_at : null,
499
+ expiresAt: credit.expires_at,
500
+ }];
501
+ }),
502
+ };
503
+ }
504
+
505
+ async function fetchCodexRateLimitResetCredits(auth: CodexAuth): Promise<{
506
+ availableCount: number | null;
507
+ availableCredits: CodexRateLimitResetCredit[];
508
+ } | null> {
509
+ try {
510
+ const resp = await fetch(CODEX_RESET_CREDITS_URL, {
511
+ headers: codexAuthHeaders(auth),
512
+ });
513
+ const text = await resp.text();
514
+ if (!resp.ok) throw new Error(`HTTP ${resp.status}: ${text.slice(0, 160)}`);
515
+ return parseCodexResetCreditDetails(JSON.parse(text) as Record<string, unknown>);
516
+ } catch (err) {
517
+ console.warn(`[Codex] reset credits lookup failed: ${(err as Error).message}`);
518
+ return null;
519
+ }
520
+ }
521
+
522
+ export async function getCodexUsageSummary(): Promise<CodexUsageSummary> {
523
+ const auth = await getCodexAuth();
524
+ if (!auth) throw new Error("missing ~/.codex/auth.json access token");
525
+
526
+ const resetCreditsPromise = fetchCodexRateLimitResetCredits(auth);
527
+
528
+ const resp = await fetch(CODEX_USAGE_URL, {
529
+ headers: { Authorization: `Bearer ${auth.accessToken}` },
530
+ });
531
+ const text = await resp.text();
532
+ if (!resp.ok) throw new Error(`HTTP ${resp.status}: ${text.slice(0, 160)}`);
533
+
534
+ const data = JSON.parse(text) as {
535
+ rate_limit?: Record<string, unknown>;
536
+ rate_limit_reset_credits?: unknown;
537
+ };
538
+ const rateLimit = data.rate_limit;
539
+ if (!rateLimit || typeof rateLimit !== "object") throw new Error("missing rate_limit");
540
+
541
+ const fiveHour = parseOptionalUsageWindow(rateLimit, ["primary_window"]);
542
+ if (!fiveHour) throw new Error("missing rate_limit.primary_window.used_percent");
543
+ const resetCredits = await resetCreditsPromise;
544
+
545
+ return {
546
+ fiveHour,
547
+ weekly: parseOptionalUsageWindow(rateLimit, [
548
+ "secondary_window",
549
+ "weekly_window",
550
+ "week_window",
551
+ "long_window",
552
+ ]),
553
+ rateLimitResetCreditsAvailable: resetCredits?.availableCount ?? parseRateLimitResetCredits(data),
554
+ rateLimitResetCredits: resetCredits?.availableCredits ?? null,
555
+ };
556
+ }
557
+
558
+ export async function consumeCodexRateLimitResetCredit(redeemRequestId: string): Promise<CodexResetConsumeResult> {
559
+ if (!redeemRequestId.trim()) throw new Error("missing redeem_request_id");
560
+ const token = await getCodexAccessToken();
561
+ if (!token) throw new Error("missing ~/.codex/auth.json access token");
562
+
563
+ const resp = await fetch(CODEX_RESET_CONSUME_URL, {
564
+ method: "POST",
565
+ headers: {
566
+ Authorization: `Bearer ${token}`,
567
+ "Content-Type": "application/json",
568
+ },
569
+ body: JSON.stringify({ redeem_request_id: redeemRequestId }),
570
+ });
571
+ const text = await resp.text();
572
+ if (!resp.ok) throw new Error(`HTTP ${resp.status}: ${text.slice(0, 160)}`);
573
+
574
+ const data = JSON.parse(text) as { code?: unknown; windows_reset?: unknown };
575
+ const code = data.code;
576
+ if (
577
+ code !== "reset"
578
+ && code !== "nothing_to_reset"
579
+ && code !== "no_credit"
580
+ && code !== "already_redeemed"
581
+ ) {
582
+ throw new Error("missing or unknown reset result code");
583
+ }
584
+ const windowsReset = Number(data.windows_reset);
585
+ return {
586
+ code,
587
+ windowsReset: Number.isFinite(windowsReset) ? Math.max(0, Math.trunc(windowsReset)) : 0,
588
+ };
589
+ }
590
+
591
+ async function resolveCodexAvatarUsage(usageHint?: CodexUsageSummary | null): Promise<CodexUsageSummary | null> {
592
+ if (usageHint !== undefined) {
593
+ if (!usageHint?.weekly) return null;
594
+ return usageHint;
595
+ }
596
+
591
597
  try {
592
598
  const summary = await getCodexUsageSummary();
593
599
  if (!summary.weekly) throw new Error("missing weekly usage window");
@@ -611,7 +617,17 @@ function cursorAvatarBatteryPercentFromUsage(summary: CursorUsageSummary): numbe
611
617
  return clampPercent(100 - apiPercentUsed);
612
618
  }
613
619
 
614
- async function fetchCursorAvatarBatteryPercent(): Promise<number | null> {
620
+ async function resolveCursorAvatarBatteryPercent(usageHint?: CursorUsageSummary | null): Promise<number | null> {
621
+ if (usageHint !== undefined) {
622
+ if (!usageHint) return null;
623
+ try {
624
+ return cursorAvatarBatteryPercentFromUsage(usageHint);
625
+ } catch (err) {
626
+ console.warn(`[${ts()}] [AVATAR] Cursor usage unavailable, using plain avatar: ${(err as Error).message}`);
627
+ return null;
628
+ }
629
+ }
630
+
615
631
  try {
616
632
  return cursorAvatarBatteryPercentFromUsage(await getCursorUsageSummary());
617
633
  } catch (err) {
@@ -814,12 +830,19 @@ async function uploadImage(
814
830
  return data.data!.image_key!;
815
831
  }
816
832
 
817
- async function getOrUploadAvatarKey(token: string, tool: string, status: string): Promise<string> {
833
+ async function getOrUploadAvatarKey(
834
+ token: string,
835
+ tool: string,
836
+ status: string,
837
+ usageHints: ChatAvatarUsageHints = {},
838
+ ): Promise<string> {
818
839
  await loadAvatarKeyCache();
819
840
  const normalizedTool = normalizeAvatarTool(tool);
820
841
  const normalizedStatus = normalizeAvatarStatus(status);
821
- const codexUsage = normalizedTool === "codex" ? await fetchCodexAvatarUsage() : null;
822
- const cursorBatteryPercent = normalizedTool === "cursor" ? await fetchCursorAvatarBatteryPercent() : null;
842
+ const codexUsage = normalizedTool === "codex" ? await resolveCodexAvatarUsage(usageHints.codexUsage) : null;
843
+ const cursorBatteryPercent = normalizedTool === "cursor"
844
+ ? await resolveCursorAvatarBatteryPercent(usageHints.cursorUsage)
845
+ : null;
823
846
  const keyName = avatarCacheKey(normalizedTool, normalizedStatus, codexUsage, cursorBatteryPercent);
824
847
  const cached = avatarKeyCache.get(keyName);
825
848
  if (cached) return cached;
@@ -832,9 +855,15 @@ async function getOrUploadAvatarKey(token: string, tool: string, status: string)
832
855
  return key;
833
856
  }
834
857
 
835
- export async function setChatAvatar(token: string, chatId: string, tool: string, status: string): Promise<void> {
858
+ export async function setChatAvatar(
859
+ token: string,
860
+ chatId: string,
861
+ tool: string,
862
+ status: string,
863
+ usageHints: ChatAvatarUsageHints = {},
864
+ ): Promise<void> {
836
865
  try {
837
- const avatarKey = await getOrUploadAvatarKey(token, tool, status);
866
+ const avatarKey = await getOrUploadAvatarKey(token, tool, status, usageHints);
838
867
  const resp = await fetch(`${BASE_URL}/im/v1/chats/${chatId}`, {
839
868
  method: "PUT",
840
869
  headers: {
package/src/index.ts CHANGED
@@ -142,8 +142,8 @@ function createFeishuAdapter(): PlatformAdapter {
142
142
  async disbandChat(chatId) {
143
143
  return disbandChat(await auth(), chatId);
144
144
  },
145
- async setChatAvatar(chatId, tool, status) {
146
- return setChatAvatar(await auth(), chatId, tool, status);
145
+ async setChatAvatar(chatId, tool, status, usageHints) {
146
+ return setChatAvatar(await auth(), chatId, tool, status, usageHints);
147
147
  },
148
148
 
149
149
  extractSessionInfo(description) {