@goodandready/dsh-clinebot 0.3.11 → 0.3.12
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/CHANGELOG.md +10 -0
- package/lib/access.js +10 -0
- package/lib/account-pool.js +131 -0
- package/lib/client.js +1113 -1141
- package/lib/cline-client.js +1 -128
- package/lib/config.js +84 -0
- package/lib/index.js +36 -864
- package/lib/provider-sync.js +216 -0
- package/lib/routes/accounts.js +79 -0
- package/lib/routes/auth.js +102 -0
- package/lib/routes/models.js +126 -0
- package/lib/routes/settings.js +102 -0
- package/lib/slash-command.js +198 -0
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,16 @@ All notable changes to `@goodandready/dsh-clinebot` will be documented in this f
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.3.12] - 2026-09-16
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
- **Full Source Code Decomposition (< 600 Lines Limit)** (Gitea Issues #41, #29):
|
|
12
|
+
- Split server-side `lib/index.js` (formerly 976 lines) into focused domain modules: `lib/access.js` (CSRF / origin security), `lib/config.js` (Schemastery configuration), `lib/provider-sync.js` (PiAi provider lifecycle & model discovery), `lib/slash-command.js` (`/cline` chat command), and `lib/routes/*` (`settings.js`, `accounts.js`, `models.js`, `auth.js`). Main entry point `lib/index.js` reduced to 148 lines.
|
|
13
|
+
- Extracted `lib/account-pool.js` from `lib/cline-client.js`, reducing it from 684 to 557 lines.
|
|
14
|
+
- Modularized client codebase into 16 clean source files in `src/client/` (< 480 lines each), separating locales, theme styles, error boundaries, and dedicated UI components.
|
|
15
|
+
- Added zero-dependency `scripts/build-client.js` maintaining single-bundle DSH Store contract (< 256 KiB limit, actual size 57.7 KiB).
|
|
16
|
+
- **Test Suite Expansion**: Added `test/decomposition.test.js` validating line limits and exported domain interfaces (45 passing tests).
|
|
17
|
+
|
|
8
18
|
## [0.3.11] - 2026-09-16
|
|
9
19
|
|
|
10
20
|
### Added
|
package/lib/access.js
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { resolveKeyValue, usageCache, clearUsageCache, clearProbeCache, DEFAULT_API_KEY_ENV } from './cline-client.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Resolve all accounts in pool with their status and keys.
|
|
5
|
+
*/
|
|
6
|
+
export async function resolveAccountPool(ctx, cfg) {
|
|
7
|
+
const apiKeyEnv = cfg?.apiKeyEnv || DEFAULT_API_KEY_ENV
|
|
8
|
+
const defaultSlot = {
|
|
9
|
+
id: 'default',
|
|
10
|
+
label: 'Default',
|
|
11
|
+
apiKeyEnv,
|
|
12
|
+
}
|
|
13
|
+
const accounts = Array.isArray(cfg?.accounts) ? cfg.accounts : []
|
|
14
|
+
const allSlots = [defaultSlot, ...accounts]
|
|
15
|
+
const activeAccount = String(cfg?.activeAccount || '')
|
|
16
|
+
const resolved = []
|
|
17
|
+
|
|
18
|
+
for (let i = 0; i < allSlots.length; i++) {
|
|
19
|
+
const slot = allSlots[i]
|
|
20
|
+
const envName = slot.apiKeyEnv || (i === 0 ? apiKeyEnv : `CLINEBOT_API_KEY_${i + 1}`)
|
|
21
|
+
const keyInfo = await resolveKeyValue(ctx, envName)
|
|
22
|
+
resolved.push({
|
|
23
|
+
id: slot.id || (i === 0 ? 'default' : `account-${i + 1}`),
|
|
24
|
+
label: slot.label || (i === 0 ? 'Default' : `Account ${i + 1}`),
|
|
25
|
+
apiKeyEnv: envName,
|
|
26
|
+
present: Boolean(keyInfo.value),
|
|
27
|
+
source: keyInfo.source,
|
|
28
|
+
value: keyInfo.value,
|
|
29
|
+
isPinned: activeAccount ? activeAccount === envName : i === 0,
|
|
30
|
+
})
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return resolved
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Check if a cached quota entry is currently exhausted.
|
|
38
|
+
* Checks 5-hour rolling limit and verifies if resetsAt timestamp has already elapsed.
|
|
39
|
+
*/
|
|
40
|
+
export function isAccountQuotaExhausted(usage) {
|
|
41
|
+
if (!usage?.windows?.fiveHour) return false
|
|
42
|
+
const fiveHour = usage.windows.fiveHour
|
|
43
|
+
if (typeof fiveHour.percentUsed !== 'number' || fiveHour.percentUsed < 95) {
|
|
44
|
+
return false
|
|
45
|
+
}
|
|
46
|
+
// Auto-recovery: if resetsAt is present and in the past, the account is recovered
|
|
47
|
+
if (fiveHour.resetsAt) {
|
|
48
|
+
const resetTime = new Date(fiveHour.resetsAt).getTime()
|
|
49
|
+
if (!Number.isNaN(resetTime) && Date.now() >= resetTime) {
|
|
50
|
+
return false
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return true
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Smart Quota-Aware Failover: rotates active account upon 429 or quota exhaustion.
|
|
58
|
+
* Prioritizes accounts with lowest percentUsed and respects resetsAt recovery.
|
|
59
|
+
*/
|
|
60
|
+
export async function rotateToNextAccount(ctx, cfg, reason = 'rate_limit', settingsApi = null) {
|
|
61
|
+
const pool = await resolveAccountPool(ctx, cfg)
|
|
62
|
+
const configured = pool.filter((acc) => acc.present && acc.value)
|
|
63
|
+
if (configured.length <= 1) {
|
|
64
|
+
return { rotated: false, reason, message: 'Pool has only 1 configured account' }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const active = String(cfg?.activeAccount || configured[0].apiKeyEnv)
|
|
68
|
+
const currentIndex = configured.findIndex((acc) => acc.apiKeyEnv === active)
|
|
69
|
+
|
|
70
|
+
// Candidate pool excluding current account if possible
|
|
71
|
+
const candidates = configured.filter((acc) => acc.apiKeyEnv !== active)
|
|
72
|
+
if (!candidates.length) {
|
|
73
|
+
return { rotated: false, reason, message: 'No alternative accounts configured' }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Assess quota for candidates if cached in memory
|
|
77
|
+
let bestCandidate = null
|
|
78
|
+
let lowestUsagePct = Infinity
|
|
79
|
+
|
|
80
|
+
for (const cand of candidates) {
|
|
81
|
+
const cacheKey = `cline:usage:${cand.value.slice(-8)}`
|
|
82
|
+
const cached = usageCache.get(cacheKey)?.data
|
|
83
|
+
const isExhausted = isAccountQuotaExhausted(cached)
|
|
84
|
+
|
|
85
|
+
if (!isExhausted) {
|
|
86
|
+
const pct = cached?.windows?.fiveHour?.percentUsed ?? 50
|
|
87
|
+
if (pct < lowestUsagePct) {
|
|
88
|
+
lowestUsagePct = pct
|
|
89
|
+
bestCandidate = cand
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Fallback if all candidates are either exhausted or uncached: pick next in round-robin
|
|
95
|
+
const nextAcc = bestCandidate || candidates[currentIndex % candidates.length] || candidates[0]
|
|
96
|
+
|
|
97
|
+
let updated = false
|
|
98
|
+
if (settingsApi?.replace) {
|
|
99
|
+
try {
|
|
100
|
+
const next = { ...cfg, activeAccount: nextAcc.apiKeyEnv }
|
|
101
|
+
await settingsApi.replace(next)
|
|
102
|
+
updated = true
|
|
103
|
+
} catch (err) {
|
|
104
|
+
ctx?.logger?.warn?.('[dsh-clinebot] Failed to persist rotated activeAccount via settingsApi: ' + (err?.message || err))
|
|
105
|
+
}
|
|
106
|
+
} else {
|
|
107
|
+
const settings = (ctx?.get && ctx.get('settings')) || ctx?.settings
|
|
108
|
+
if (settings?.mutate) {
|
|
109
|
+
try {
|
|
110
|
+
await settings.mutate('dsh-clinebot', [
|
|
111
|
+
{ op: 'set', path: ['activeAccount'], value: nextAcc.apiKeyEnv },
|
|
112
|
+
])
|
|
113
|
+
updated = true
|
|
114
|
+
} catch (err) {
|
|
115
|
+
ctx?.logger?.warn?.('[dsh-clinebot] Failed to persist rotated activeAccount via settings.mutate: ' + (err?.message || err))
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Reset cached quota and host probes so new account immediately revalidates
|
|
121
|
+
clearUsageCache()
|
|
122
|
+
clearProbeCache()
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
rotated: true,
|
|
126
|
+
previousAccount: active,
|
|
127
|
+
activeAccount: nextAcc.apiKeyEnv,
|
|
128
|
+
reason,
|
|
129
|
+
updatedSettings: updated,
|
|
130
|
+
}
|
|
131
|
+
}
|