@algochad/archcoder 2.0.2

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 (157) hide show
  1. package/README.md +113 -0
  2. package/bin/cli-entry.js +55 -0
  3. package/bin/cli-output.js +145 -0
  4. package/bin/cli.js +5108 -0
  5. package/bin/cli.test.js +56 -0
  6. package/dist/apple-touch-icon-120x120.png +0 -0
  7. package/dist/apple-touch-icon-152x152.png +0 -0
  8. package/dist/apple-touch-icon-167x167.png +0 -0
  9. package/dist/apple-touch-icon-180x180.png +0 -0
  10. package/dist/apple-touch-icon.png +0 -0
  11. package/dist/apple-touch-icon.svg +67 -0
  12. package/dist/assets/MultiRunWindow-BZp3MjJP.js +1 -0
  13. package/dist/assets/SettingsWindow-DoGYXpX7.js +1 -0
  14. package/dist/assets/TerminalView-BN7BR5Ff.js +3 -0
  15. package/dist/assets/TimelineDialog-ZQ33oVQR.js +1 -0
  16. package/dist/assets/ToolOutputDialog-Blv3pnug.js +16 -0
  17. package/dist/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
  18. package/dist/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
  19. package/dist/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
  20. package/dist/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
  21. package/dist/assets/ibm-plex-mono-latin-600-normal-BgSNZQsw.woff2 +0 -0
  22. package/dist/assets/ibm-plex-mono-latin-600-normal-DWFSQ4vo.woff +0 -0
  23. package/dist/assets/ibm-plex-sans-latin-400-normal-CDDApCn2.woff2 +0 -0
  24. package/dist/assets/ibm-plex-sans-latin-400-normal-CYLoc0-x.woff +0 -0
  25. package/dist/assets/ibm-plex-sans-latin-500-normal-6ng42L7E.woff2 +0 -0
  26. package/dist/assets/ibm-plex-sans-latin-500-normal-BgVn5rGT.woff +0 -0
  27. package/dist/assets/ibm-plex-sans-latin-600-normal-Cu4Hd6ag.woff +0 -0
  28. package/dist/assets/ibm-plex-sans-latin-600-normal-CuJfVYMP.woff2 +0 -0
  29. package/dist/assets/index-CtCEGYrr.css +1 -0
  30. package/dist/assets/index-o_d2wtWC.js +48 -0
  31. package/dist/assets/main-5QGBtzdq.css +1 -0
  32. package/dist/assets/main-B6oiMU86.js +8033 -0
  33. package/dist/assets/vendor--DbVqbJpV.css +1 -0
  34. package/dist/assets/vendor-.bun-HTKwyaEM.js +10086 -0
  35. package/dist/assets/wasm-CG6Dc4jp.js +1 -0
  36. package/dist/assets/worker-bqd4RMrj.js +155 -0
  37. package/dist/favicon-16.png +0 -0
  38. package/dist/favicon-32.png +0 -0
  39. package/dist/favicon.png +0 -0
  40. package/dist/favicon.svg +67 -0
  41. package/dist/index.html +533 -0
  42. package/dist/logo-dark-192x192.png +0 -0
  43. package/dist/logo-dark-512x512.svg +16 -0
  44. package/dist/logo-light-192x192.png +0 -0
  45. package/dist/logo-light-512x512.svg +16 -0
  46. package/dist/pwa-192.png +0 -0
  47. package/dist/pwa-512.png +0 -0
  48. package/dist/pwa-maskable-192.png +0 -0
  49. package/dist/pwa-maskable-512.png +0 -0
  50. package/dist/site.webmanifest +22 -0
  51. package/dist/sw.js +1 -0
  52. package/package.json +107 -0
  53. package/public/apple-touch-icon-120x120.png +0 -0
  54. package/public/apple-touch-icon-152x152.png +0 -0
  55. package/public/apple-touch-icon-167x167.png +0 -0
  56. package/public/apple-touch-icon-180x180.png +0 -0
  57. package/public/apple-touch-icon.png +0 -0
  58. package/public/apple-touch-icon.svg +67 -0
  59. package/public/favicon-16.png +0 -0
  60. package/public/favicon-32.png +0 -0
  61. package/public/favicon.png +0 -0
  62. package/public/favicon.svg +67 -0
  63. package/public/logo-dark-192x192.png +0 -0
  64. package/public/logo-dark-512x512.svg +16 -0
  65. package/public/logo-light-192x192.png +0 -0
  66. package/public/logo-light-512x512.svg +16 -0
  67. package/public/pwa-192.png +0 -0
  68. package/public/pwa-512.png +0 -0
  69. package/public/pwa-maskable-192.png +0 -0
  70. package/public/pwa-maskable-512.png +0 -0
  71. package/public/site.webmanifest +22 -0
  72. package/server/TERMINAL_INPUT_WS_PROTOCOL.md +44 -0
  73. package/server/index.d.ts +37 -0
  74. package/server/index.js +14694 -0
  75. package/server/lib/cloudflare-tunnel.js +650 -0
  76. package/server/lib/git/DOCUMENTATION.md +146 -0
  77. package/server/lib/git/credentials.js +74 -0
  78. package/server/lib/git/identity-storage.js +110 -0
  79. package/server/lib/git/index.js +6 -0
  80. package/server/lib/git/service.js +3117 -0
  81. package/server/lib/github/DOCUMENTATION.md +170 -0
  82. package/server/lib/github/auth.js +307 -0
  83. package/server/lib/github/device-flow.js +50 -0
  84. package/server/lib/github/index.js +24 -0
  85. package/server/lib/github/octokit.js +10 -0
  86. package/server/lib/github/pr-status.js +478 -0
  87. package/server/lib/github/repo/index.js +55 -0
  88. package/server/lib/installer/desktop.js +289 -0
  89. package/server/lib/installer/download.js +208 -0
  90. package/server/lib/installer/index.js +45 -0
  91. package/server/lib/installer/platform.js +100 -0
  92. package/server/lib/notifications/DOCUMENTATION.md +61 -0
  93. package/server/lib/notifications/index.js +1 -0
  94. package/server/lib/notifications/message.js +49 -0
  95. package/server/lib/notifications/message.test.js +59 -0
  96. package/server/lib/opencode/DOCUMENTATION.md +59 -0
  97. package/server/lib/opencode/agents.js +634 -0
  98. package/server/lib/opencode/auth.js +81 -0
  99. package/server/lib/opencode/commands.js +339 -0
  100. package/server/lib/opencode/index.js +66 -0
  101. package/server/lib/opencode/mcp.js +206 -0
  102. package/server/lib/opencode/providers.js +96 -0
  103. package/server/lib/opencode/shared.js +527 -0
  104. package/server/lib/opencode/skills.js +480 -0
  105. package/server/lib/opencode/tunnel-auth.js +591 -0
  106. package/server/lib/opencode/ui-auth.js +510 -0
  107. package/server/lib/package-manager.js +505 -0
  108. package/server/lib/quota/DOCUMENTATION.md +55 -0
  109. package/server/lib/quota/index.js +24 -0
  110. package/server/lib/quota/providers/claude.js +107 -0
  111. package/server/lib/quota/providers/codex.js +113 -0
  112. package/server/lib/quota/providers/copilot.js +165 -0
  113. package/server/lib/quota/providers/google/api.js +92 -0
  114. package/server/lib/quota/providers/google/auth.js +108 -0
  115. package/server/lib/quota/providers/google/index.js +124 -0
  116. package/server/lib/quota/providers/google/transforms.js +109 -0
  117. package/server/lib/quota/providers/index.js +152 -0
  118. package/server/lib/quota/providers/interface.js +55 -0
  119. package/server/lib/quota/providers/kimi.js +108 -0
  120. package/server/lib/quota/providers/minimax-cn-coding-plan.js +15 -0
  121. package/server/lib/quota/providers/minimax-coding-plan.js +15 -0
  122. package/server/lib/quota/providers/minimax-shared.js +136 -0
  123. package/server/lib/quota/providers/nanogpt.js +124 -0
  124. package/server/lib/quota/providers/ollama-cloud.js +112 -0
  125. package/server/lib/quota/providers/openai.js +91 -0
  126. package/server/lib/quota/providers/openrouter.js +92 -0
  127. package/server/lib/quota/providers/zai.js +91 -0
  128. package/server/lib/quota/utils/auth.js +46 -0
  129. package/server/lib/quota/utils/formatters.js +76 -0
  130. package/server/lib/quota/utils/index.js +10 -0
  131. package/server/lib/quota/utils/transformers.js +55 -0
  132. package/server/lib/skills-catalog/DOCUMENTATION.md +178 -0
  133. package/server/lib/skills-catalog/cache.js +32 -0
  134. package/server/lib/skills-catalog/clawdhub/api.js +158 -0
  135. package/server/lib/skills-catalog/clawdhub/index.js +30 -0
  136. package/server/lib/skills-catalog/clawdhub/install.js +238 -0
  137. package/server/lib/skills-catalog/clawdhub/scan.js +113 -0
  138. package/server/lib/skills-catalog/curated-sources.js +21 -0
  139. package/server/lib/skills-catalog/git.js +77 -0
  140. package/server/lib/skills-catalog/index.js +42 -0
  141. package/server/lib/skills-catalog/install.js +294 -0
  142. package/server/lib/skills-catalog/scan.js +221 -0
  143. package/server/lib/skills-catalog/source.js +85 -0
  144. package/server/lib/terminal/DOCUMENTATION.md +114 -0
  145. package/server/lib/terminal/index.js +12 -0
  146. package/server/lib/terminal/input-ws-protocol.js +66 -0
  147. package/server/lib/terminal/input-ws-protocol.test.js +138 -0
  148. package/server/lib/tts/DOCUMENTATION.md +134 -0
  149. package/server/lib/tts/index.js +16 -0
  150. package/server/lib/tts/service.js +162 -0
  151. package/server/lib/tts/summarization.js +171 -0
  152. package/server/lib/tunnels/index.js +166 -0
  153. package/server/lib/tunnels/providers/cloudflare.js +260 -0
  154. package/server/lib/tunnels/registry.js +51 -0
  155. package/server/lib/tunnels/types.js +219 -0
  156. package/server/lib/utils/lru.js +107 -0
  157. package/server/lib/utils/sse.js +121 -0
@@ -0,0 +1,112 @@
1
+ import { homedir } from 'os';
2
+ import { readFileSync, existsSync } from 'fs';
3
+ import { join } from 'path';
4
+ import { buildResult, toUsageWindow, toNumber } from '../utils/index.js';
5
+
6
+ const COOKIE_PATH = join(homedir(), '.config', 'ollama-quota', 'cookie');
7
+
8
+ export const providerId = 'ollama-cloud';
9
+ export const providerName = 'Ollama Cloud';
10
+ export const aliases = ['ollama-cloud', 'ollamacloud'];
11
+
12
+ const readCookieFile = () => {
13
+ try {
14
+ if (!existsSync(COOKIE_PATH)) return null;
15
+ const content = readFileSync(COOKIE_PATH, 'utf-8');
16
+ const trimmed = content.trim();
17
+ return trimmed || null;
18
+ } catch {
19
+ return null;
20
+ }
21
+ };
22
+
23
+ const parseOllamaSettingsHtml = (html) => {
24
+ const windows = {};
25
+ const sessionMatch = html.match(/Session\s+usage[^0-9]*([0-9.]+)%/i);
26
+ if (sessionMatch) {
27
+ windows.session = toUsageWindow({
28
+ usedPercent: toNumber(sessionMatch[1]),
29
+ windowSeconds: null,
30
+ resetAt: null
31
+ });
32
+ }
33
+ const weeklyMatch = html.match(/Weekly\s+usage[^0-9]*([0-9.]+)%/i);
34
+ if (weeklyMatch) {
35
+ windows.weekly = toUsageWindow({
36
+ usedPercent: toNumber(weeklyMatch[1]),
37
+ windowSeconds: null,
38
+ resetAt: null
39
+ });
40
+ }
41
+ const premiumMatch = html.match(/Premium[^0-9]*([0-9]+)\s*\/\s*([0-9]+)/i);
42
+ if (premiumMatch) {
43
+ const used = toNumber(premiumMatch[1]);
44
+ const total = toNumber(premiumMatch[2]);
45
+ const usedPercent = total && used !== null ? Math.min(100, (used / total) * 100) : null;
46
+ windows.premium = toUsageWindow({
47
+ usedPercent,
48
+ windowSeconds: null,
49
+ resetAt: null,
50
+ valueLabel: `${used ?? 0} / ${total ?? 0}`
51
+ });
52
+ }
53
+ return windows;
54
+ };
55
+
56
+ export const isConfigured = () => {
57
+ const cookie = readCookieFile();
58
+ return Boolean(cookie);
59
+ };
60
+
61
+ export const fetchQuota = async () => {
62
+ const cookie = readCookieFile();
63
+
64
+ if (!cookie) {
65
+ return buildResult({
66
+ providerId,
67
+ providerName,
68
+ ok: false,
69
+ configured: false,
70
+ error: 'Not configured'
71
+ });
72
+ }
73
+
74
+ try {
75
+ const response = await fetch('https://ollama.com/settings', {
76
+ method: 'GET',
77
+ headers: {
78
+ Cookie: cookie,
79
+ 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36'
80
+ }
81
+ });
82
+
83
+ if (!response.ok) {
84
+ return buildResult({
85
+ providerId,
86
+ providerName,
87
+ ok: false,
88
+ configured: true,
89
+ error: `API error: ${response.status}`
90
+ });
91
+ }
92
+
93
+ const html = await response.text();
94
+ const windows = parseOllamaSettingsHtml(html);
95
+
96
+ return buildResult({
97
+ providerId,
98
+ providerName,
99
+ ok: true,
100
+ configured: true,
101
+ usage: { windows }
102
+ });
103
+ } catch (error) {
104
+ return buildResult({
105
+ providerId,
106
+ providerName,
107
+ ok: false,
108
+ configured: true,
109
+ error: error instanceof Error ? error.message : 'Request failed'
110
+ });
111
+ }
112
+ };
@@ -0,0 +1,91 @@
1
+ import { readAuthFile } from '../../opencode/auth.js';
2
+ import {
3
+ getAuthEntry,
4
+ normalizeAuthEntry,
5
+ buildResult,
6
+ toUsageWindow,
7
+ toNumber,
8
+ toTimestamp
9
+ } from '../utils/index.js';
10
+
11
+ export const providerId = 'openai';
12
+ export const providerName = 'OpenAI';
13
+ export const aliases = ['openai', 'codex', 'chatgpt'];
14
+
15
+ export const isConfigured = () => {
16
+ const auth = readAuthFile();
17
+ const entry = normalizeAuthEntry(getAuthEntry(auth, aliases));
18
+ return Boolean(entry?.access || entry?.token);
19
+ };
20
+
21
+ export const fetchQuota = async () => {
22
+ const auth = readAuthFile();
23
+ const entry = normalizeAuthEntry(getAuthEntry(auth, aliases));
24
+ const accessToken = entry?.access ?? entry?.token;
25
+
26
+ if (!accessToken) {
27
+ return buildResult({
28
+ providerId,
29
+ providerName,
30
+ ok: false,
31
+ configured: false,
32
+ error: 'Not configured'
33
+ });
34
+ }
35
+
36
+ try {
37
+ const response = await fetch('https://chatgpt.com/backend-api/wham/usage', {
38
+ method: 'GET',
39
+ headers: {
40
+ Authorization: `Bearer ${accessToken}`,
41
+ 'Content-Type': 'application/json'
42
+ }
43
+ });
44
+
45
+ if (!response.ok) {
46
+ return buildResult({
47
+ providerId,
48
+ providerName,
49
+ ok: false,
50
+ configured: true,
51
+ error: `API error: ${response.status}`
52
+ });
53
+ }
54
+
55
+ const payload = await response.json();
56
+ const primary = payload?.rate_limit?.primary_window ?? null;
57
+ const secondary = payload?.rate_limit?.secondary_window ?? null;
58
+
59
+ const windows = {};
60
+ if (primary) {
61
+ windows['5h'] = toUsageWindow({
62
+ usedPercent: primary.used_percent ?? null,
63
+ windowSeconds: primary.limit_window_seconds ?? null,
64
+ resetAt: primary.reset_at ? primary.reset_at * 1000 : null
65
+ });
66
+ }
67
+ if (secondary) {
68
+ windows['weekly'] = toUsageWindow({
69
+ usedPercent: secondary.used_percent ?? null,
70
+ windowSeconds: secondary.limit_window_seconds ?? null,
71
+ resetAt: secondary.reset_at ? secondary.reset_at * 1000 : null
72
+ });
73
+ }
74
+
75
+ return buildResult({
76
+ providerId,
77
+ providerName,
78
+ ok: true,
79
+ configured: true,
80
+ usage: { windows }
81
+ });
82
+ } catch (error) {
83
+ return buildResult({
84
+ providerId,
85
+ providerName,
86
+ ok: false,
87
+ configured: true,
88
+ error: error instanceof Error ? error.message : 'Request failed'
89
+ });
90
+ }
91
+ };
@@ -0,0 +1,92 @@
1
+ import { readAuthFile } from '../../opencode/auth.js';
2
+ import {
3
+ getAuthEntry,
4
+ normalizeAuthEntry,
5
+ buildResult,
6
+ toUsageWindow,
7
+ toNumber,
8
+ formatMoney
9
+ } from '../utils/index.js';
10
+
11
+ export const providerId = 'openrouter';
12
+ export const providerName = 'OpenRouter';
13
+ export const aliases = ['openrouter'];
14
+
15
+ export const isConfigured = () => {
16
+ const auth = readAuthFile();
17
+ const entry = normalizeAuthEntry(getAuthEntry(auth, aliases));
18
+ return Boolean(entry?.key || entry?.token);
19
+ };
20
+
21
+ export const fetchQuota = async () => {
22
+ const auth = readAuthFile();
23
+ const entry = normalizeAuthEntry(getAuthEntry(auth, aliases));
24
+ const apiKey = entry?.key ?? entry?.token;
25
+
26
+ if (!apiKey) {
27
+ return buildResult({
28
+ providerId,
29
+ providerName,
30
+ ok: false,
31
+ configured: false,
32
+ error: 'Not configured'
33
+ });
34
+ }
35
+
36
+ try {
37
+ const response = await fetch('https://openrouter.ai/api/v1/credits', {
38
+ method: 'GET',
39
+ headers: {
40
+ Authorization: `Bearer ${apiKey}`,
41
+ 'Content-Type': 'application/json'
42
+ }
43
+ });
44
+
45
+ if (!response.ok) {
46
+ return buildResult({
47
+ providerId,
48
+ providerName,
49
+ ok: false,
50
+ configured: true,
51
+ error: `API error: ${response.status}`
52
+ });
53
+ }
54
+
55
+ const payload = await response.json();
56
+ const credits = payload?.data ?? {};
57
+ const totalCredits = toNumber(credits.total_credits);
58
+ const totalUsage = toNumber(credits.total_usage);
59
+ const remaining = totalCredits !== null && totalUsage !== null
60
+ ? Math.max(0, totalCredits - totalUsage)
61
+ : null;
62
+ const usedPercent = totalCredits && totalUsage !== null
63
+ ? Math.max(0, Math.min(100, (totalUsage / totalCredits) * 100))
64
+ : null;
65
+ const valueLabel = remaining !== null ? `$${formatMoney(remaining)} remaining` : null;
66
+
67
+ const windows = {
68
+ credits: toUsageWindow({
69
+ usedPercent,
70
+ windowSeconds: null,
71
+ resetAt: null,
72
+ valueLabel
73
+ })
74
+ };
75
+
76
+ return buildResult({
77
+ providerId,
78
+ providerName,
79
+ ok: true,
80
+ configured: true,
81
+ usage: { windows }
82
+ });
83
+ } catch (error) {
84
+ return buildResult({
85
+ providerId,
86
+ providerName,
87
+ ok: false,
88
+ configured: true,
89
+ error: error instanceof Error ? error.message : 'Request failed'
90
+ });
91
+ }
92
+ };
@@ -0,0 +1,91 @@
1
+ import { readAuthFile } from '../../opencode/auth.js';
2
+ import {
3
+ getAuthEntry,
4
+ normalizeAuthEntry,
5
+ buildResult,
6
+ toUsageWindow,
7
+ toNumber,
8
+ toTimestamp,
9
+ resolveWindowSeconds,
10
+ resolveWindowLabel,
11
+ normalizeTimestamp
12
+ } from '../utils/index.js';
13
+
14
+ export const providerId = 'zai-coding-plan';
15
+ export const providerName = 'z.ai';
16
+ export const aliases = ['zai-coding-plan', 'zai', 'z.ai'];
17
+
18
+ export const isConfigured = () => {
19
+ const auth = readAuthFile();
20
+ const entry = normalizeAuthEntry(getAuthEntry(auth, aliases));
21
+ return Boolean(entry?.key || entry?.token);
22
+ };
23
+
24
+ export const fetchQuota = async () => {
25
+ const auth = readAuthFile();
26
+ const entry = normalizeAuthEntry(getAuthEntry(auth, aliases));
27
+ const apiKey = entry?.key ?? entry?.token;
28
+
29
+ if (!apiKey) {
30
+ return buildResult({
31
+ providerId,
32
+ providerName,
33
+ ok: false,
34
+ configured: false,
35
+ error: 'Not configured'
36
+ });
37
+ }
38
+
39
+ try {
40
+ const response = await fetch('https://api.z.ai/api/monitor/usage/quota/limit', {
41
+ method: 'GET',
42
+ headers: {
43
+ Authorization: `Bearer ${apiKey}`,
44
+ 'Content-Type': 'application/json'
45
+ }
46
+ });
47
+
48
+ if (!response.ok) {
49
+ return buildResult({
50
+ providerId,
51
+ providerName,
52
+ ok: false,
53
+ configured: true,
54
+ error: `API error: ${response.status}`
55
+ });
56
+ }
57
+
58
+ const payload = await response.json();
59
+ const limits = Array.isArray(payload?.data?.limits) ? payload.data.limits : [];
60
+ const tokensLimit = limits.find((limit) => limit?.type === 'TOKENS_LIMIT');
61
+ const windowSeconds = resolveWindowSeconds(tokensLimit);
62
+ const windowLabel = resolveWindowLabel(windowSeconds);
63
+ const resetAt = tokensLimit?.nextResetTime ? normalizeTimestamp(tokensLimit.nextResetTime) : null;
64
+ const usedPercent = typeof tokensLimit?.percentage === 'number' ? tokensLimit.percentage : null;
65
+
66
+ const windows = {};
67
+ if (tokensLimit) {
68
+ windows[windowLabel] = toUsageWindow({
69
+ usedPercent,
70
+ windowSeconds,
71
+ resetAt
72
+ });
73
+ }
74
+
75
+ return buildResult({
76
+ providerId,
77
+ providerName,
78
+ ok: true,
79
+ configured: true,
80
+ usage: { windows }
81
+ });
82
+ } catch (error) {
83
+ return buildResult({
84
+ providerId,
85
+ providerName,
86
+ ok: false,
87
+ configured: true,
88
+ error: error instanceof Error ? error.message : 'Request failed'
89
+ });
90
+ }
91
+ };
@@ -0,0 +1,46 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import os from 'os';
4
+
5
+ const OPENCODE_CONFIG_DIR = path.join(os.homedir(), '.config', 'opencode');
6
+ const OPENCODE_DATA_DIR = path.join(os.homedir(), '.local', 'share', 'opencode');
7
+
8
+ export const ANTIGRAVITY_ACCOUNTS_PATHS = [
9
+ path.join(OPENCODE_CONFIG_DIR, 'antigravity-accounts.json'),
10
+ path.join(OPENCODE_DATA_DIR, 'antigravity-accounts.json')
11
+ ];
12
+
13
+ export const readJsonFile = (filePath) => {
14
+ if (!fs.existsSync(filePath)) {
15
+ return null;
16
+ }
17
+ try {
18
+ const raw = fs.readFileSync(filePath, 'utf8');
19
+ const trimmed = raw.trim();
20
+ if (!trimmed) return null;
21
+ return JSON.parse(trimmed);
22
+ } catch (error) {
23
+ console.warn(`Failed to read JSON file: ${filePath}`, error);
24
+ return null;
25
+ }
26
+ };
27
+
28
+ export const getAuthEntry = (auth, aliases) => {
29
+ for (const alias of aliases) {
30
+ if (auth[alias]) {
31
+ return auth[alias];
32
+ }
33
+ }
34
+ return null;
35
+ };
36
+
37
+ export const normalizeAuthEntry = (entry) => {
38
+ if (!entry) return null;
39
+ if (typeof entry === 'string') {
40
+ return { token: entry };
41
+ }
42
+ if (typeof entry === 'object') {
43
+ return entry;
44
+ }
45
+ return null;
46
+ };
@@ -0,0 +1,76 @@
1
+ export const formatResetTime = (timestamp) => {
2
+ try {
3
+ const resetDate = new Date(timestamp);
4
+ const now = new Date();
5
+ const isToday = resetDate.toDateString() === now.toDateString();
6
+
7
+ if (isToday) {
8
+ return resetDate.toLocaleTimeString(undefined, {
9
+ hour: 'numeric',
10
+ minute: '2-digit'
11
+ });
12
+ }
13
+
14
+ return resetDate.toLocaleString(undefined, {
15
+ month: 'short',
16
+ day: 'numeric',
17
+ weekday: 'short',
18
+ hour: 'numeric',
19
+ minute: '2-digit'
20
+ });
21
+ } catch {
22
+ return null;
23
+ }
24
+ };
25
+
26
+ export const calculateResetAfterSeconds = (resetAt) => {
27
+ if (!resetAt) return null;
28
+ const delta = Math.floor((resetAt - Date.now()) / 1000);
29
+ return delta < 0 ? 0 : delta;
30
+ };
31
+
32
+ export const toUsageWindow = ({ usedPercent, windowSeconds, resetAt, valueLabel }) => {
33
+ const resetAfterSeconds = calculateResetAfterSeconds(resetAt);
34
+ const resetFormatted = resetAt ? formatResetTime(resetAt) : null;
35
+ return {
36
+ usedPercent,
37
+ remainingPercent: usedPercent !== null ? Math.max(0, 100 - usedPercent) : null,
38
+ windowSeconds: windowSeconds ?? null,
39
+ resetAfterSeconds,
40
+ resetAt,
41
+ resetAtFormatted: resetFormatted,
42
+ resetAfterFormatted: resetFormatted,
43
+ ...(valueLabel ? { valueLabel } : {})
44
+ };
45
+ };
46
+
47
+ export const buildResult = ({ providerId, providerName, ok, configured, usage, error }) => ({
48
+ providerId,
49
+ providerName,
50
+ ok,
51
+ configured,
52
+ usage: usage ?? null,
53
+ ...(error ? { error } : {}),
54
+ fetchedAt: Date.now()
55
+ });
56
+
57
+ export const durationToLabel = (duration, unit) => {
58
+ if (!duration || !unit) return 'limit';
59
+ if (unit === 'TIME_UNIT_MINUTE') return `${duration}m`;
60
+ if (unit === 'TIME_UNIT_HOUR') return `${duration}h`;
61
+ if (unit === 'TIME_UNIT_DAY') return `${duration}d`;
62
+ return 'limit';
63
+ };
64
+
65
+ export const durationToSeconds = (duration, unit) => {
66
+ if (!duration || !unit) return null;
67
+ if (unit === 'TIME_UNIT_MINUTE') return duration * 60;
68
+ if (unit === 'TIME_UNIT_HOUR') return duration * 3600;
69
+ if (unit === 'TIME_UNIT_DAY') return duration * 86400;
70
+ return null;
71
+ };
72
+
73
+ export const formatMoney = (value) => {
74
+ if (typeof value !== 'number' || !Number.isFinite(value)) return null;
75
+ return value.toFixed(2);
76
+ };
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Quota Utilities
3
+ *
4
+ * Shared utility functions for quota calculations and formatting.
5
+ * @module quota/utils
6
+ */
7
+
8
+ export * from './auth.js';
9
+ export * from './transformers.js';
10
+ export * from './formatters.js';
@@ -0,0 +1,55 @@
1
+ export const asObject = (value) => (value && typeof value === 'object' ? value : null);
2
+
3
+ export const asNonEmptyString = (value) => {
4
+ if (typeof value !== 'string') return null;
5
+ const trimmed = value.trim();
6
+ return trimmed ? trimmed : null;
7
+ };
8
+
9
+ export const toNumber = (value) => {
10
+ if (typeof value === 'number' && Number.isFinite(value)) {
11
+ return value;
12
+ }
13
+ if (typeof value === 'string') {
14
+ const parsed = Number(value);
15
+ return Number.isFinite(parsed) ? parsed : null;
16
+ }
17
+ return null;
18
+ };
19
+
20
+ export const toTimestamp = (value) => {
21
+ if (!value) return null;
22
+ if (typeof value === 'number') {
23
+ return value < 1_000_000_000_000 ? value * 1000 : value;
24
+ }
25
+ if (typeof value === 'string') {
26
+ const parsed = Date.parse(value);
27
+ return Number.isNaN(parsed) ? null : parsed;
28
+ }
29
+ return null;
30
+ };
31
+
32
+ export const normalizeTimestamp = (value) => {
33
+ if (typeof value !== 'number') return null;
34
+ return value < 1_000_000_000_000 ? value * 1000 : value;
35
+ };
36
+
37
+ export const resolveWindowSeconds = (limit) => {
38
+ const ZAI_TOKEN_WINDOW_SECONDS = { 3: 3600 };
39
+ if (!limit || !limit.number) return null;
40
+ const unitSeconds = ZAI_TOKEN_WINDOW_SECONDS[limit.unit];
41
+ if (!unitSeconds) return null;
42
+ return unitSeconds * limit.number;
43
+ };
44
+
45
+ export const resolveWindowLabel = (windowSeconds) => {
46
+ if (!windowSeconds) return 'tokens';
47
+ if (windowSeconds % 86400 === 0) {
48
+ const days = windowSeconds / 86400;
49
+ return days === 7 ? 'weekly' : `${days}d`;
50
+ }
51
+ if (windowSeconds % 3600 === 0) {
52
+ return `${windowSeconds / 3600}h`;
53
+ }
54
+ return `${windowSeconds}s`;
55
+ };