2026-09-08 17:02:56 +02:00
|
|
|
import { readAuthFile } from '../../opencode/auth.js';
|
|
|
|
|
import {
|
|
|
|
|
getAuthEntry,
|
|
|
|
|
normalizeAuthEntry,
|
|
|
|
|
asObject,
|
2026-09-09 17:51:39 +03:00
|
|
|
asNonEmptyString,
|
2026-09-08 17:02:56 +02:00
|
|
|
buildResult,
|
|
|
|
|
toUsageWindow,
|
|
|
|
|
toNumber,
|
|
|
|
|
toTimestamp
|
|
|
|
|
} from '../utils/index.js';
|
|
|
|
|
|
|
|
|
|
export const providerId = 'cline-pass';
|
|
|
|
|
export const providerName = 'ClinePass';
|
2026-09-09 17:51:39 +03:00
|
|
|
export const aliases = ['cline-pass'];
|
2026-09-08 17:02:56 +02:00
|
|
|
const CLINE_USAGE_URL = 'https://api.cline.bot/api/v1/users/me/plan/usage-limits';
|
|
|
|
|
|
|
|
|
|
// Cline reports a rolling five-hour window, a rolling weekly window, and a
|
|
|
|
|
// calendar-month limit. Each window carries its duration so consumers can rank
|
|
|
|
|
// limits by how soon they run out; the calendar month has no fixed duration.
|
2026-09-09 17:51:39 +03:00
|
|
|
const WINDOW_KINDS = new Map([
|
|
|
|
|
['five_hour', { key: '5h', windowSeconds: 5 * 60 * 60 }],
|
|
|
|
|
['weekly', { key: 'weekly', windowSeconds: 7 * 24 * 60 * 60 }],
|
|
|
|
|
['monthly', { key: 'monthly', windowSeconds: null }]
|
|
|
|
|
]);
|
2026-09-08 17:02:56 +02:00
|
|
|
|
2026-09-09 17:51:39 +03:00
|
|
|
const getApiKey = (auth) => {
|
2026-09-08 17:02:56 +02:00
|
|
|
const entry = normalizeAuthEntry(getAuthEntry(auth, aliases));
|
2026-09-09 17:51:39 +03:00
|
|
|
return asNonEmptyString(entry?.key) ?? asNonEmptyString(entry?.token);
|
2026-09-08 17:02:56 +02:00
|
|
|
};
|
|
|
|
|
|
2026-09-09 17:51:39 +03:00
|
|
|
export const isConfigured = (auth = readAuthFile()) => Boolean(getApiKey(auth));
|
|
|
|
|
|
|
|
|
|
export const fetchQuota = async ({ readAuth = readAuthFile, fetchImpl = fetch } = {}) => {
|
|
|
|
|
const apiKey = getApiKey(readAuth());
|
2026-09-08 17:02:56 +02:00
|
|
|
|
|
|
|
|
if (!apiKey) {
|
|
|
|
|
return buildResult({
|
|
|
|
|
providerId,
|
|
|
|
|
providerName,
|
|
|
|
|
ok: false,
|
|
|
|
|
configured: false,
|
|
|
|
|
error: 'Not configured'
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const timeoutSignal = AbortSignal.timeout(15_000);
|
|
|
|
|
|
|
|
|
|
try {
|
2026-09-09 17:51:39 +03:00
|
|
|
const response = await fetchImpl(CLINE_USAGE_URL, {
|
2026-09-08 17:02:56 +02:00
|
|
|
method: 'GET',
|
|
|
|
|
headers: {
|
|
|
|
|
Authorization: `Bearer ${apiKey}`,
|
|
|
|
|
'Accept-Encoding': 'identity'
|
|
|
|
|
},
|
|
|
|
|
signal: timeoutSignal
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
return buildResult({
|
|
|
|
|
providerId,
|
|
|
|
|
providerName,
|
|
|
|
|
ok: false,
|
|
|
|
|
configured: true,
|
|
|
|
|
error: response.status === 401
|
|
|
|
|
? 'Session expired — please re-authenticate with ClinePass'
|
|
|
|
|
: `API error: ${response.status}`
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-09 17:51:39 +03:00
|
|
|
const payload = asObject(await response.json());
|
|
|
|
|
const data = asObject(payload?.data);
|
|
|
|
|
const limits = Array.isArray(data?.limits) ? data.limits : [];
|
2026-09-08 17:02:56 +02:00
|
|
|
|
|
|
|
|
const windows = {};
|
|
|
|
|
for (const item of limits) {
|
|
|
|
|
const limit = asObject(item);
|
|
|
|
|
if (!limit) continue;
|
2026-09-09 17:51:39 +03:00
|
|
|
const kind = WINDOW_KINDS.get(asNonEmptyString(limit.type));
|
2026-09-08 17:02:56 +02:00
|
|
|
if (!kind) continue;
|
2026-09-09 17:51:39 +03:00
|
|
|
const usedPercent = toNumber(asNonEmptyString(limit.percentUsed)
|
|
|
|
|
?? (Number.isFinite(limit.percentUsed) ? limit.percentUsed : null));
|
2026-09-08 17:02:56 +02:00
|
|
|
if (usedPercent === null) continue;
|
|
|
|
|
windows[kind.key] = toUsageWindow({
|
|
|
|
|
usedPercent,
|
|
|
|
|
windowSeconds: kind.windowSeconds,
|
|
|
|
|
resetAt: toTimestamp(limit.resetsAt)
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (Object.keys(windows).length === 0) {
|
|
|
|
|
return buildResult({
|
|
|
|
|
providerId,
|
|
|
|
|
providerName,
|
|
|
|
|
ok: false,
|
|
|
|
|
configured: true,
|
|
|
|
|
error: 'No quota data in response'
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return buildResult({
|
|
|
|
|
providerId,
|
|
|
|
|
providerName,
|
|
|
|
|
ok: true,
|
|
|
|
|
configured: true,
|
|
|
|
|
usage: { windows }
|
|
|
|
|
});
|
|
|
|
|
} catch (error) {
|
2026-09-09 17:51:39 +03:00
|
|
|
const isTimeout = error instanceof DOMException && (
|
|
|
|
|
error.name === 'TimeoutError' || (error.name === 'AbortError' && timeoutSignal.aborted)
|
|
|
|
|
);
|
2026-09-08 17:02:56 +02:00
|
|
|
const isParseError = error instanceof SyntaxError;
|
|
|
|
|
return buildResult({
|
|
|
|
|
providerId,
|
|
|
|
|
providerName,
|
|
|
|
|
ok: false,
|
|
|
|
|
configured: true,
|
|
|
|
|
error: isTimeout
|
|
|
|
|
? 'Request timed out'
|
|
|
|
|
: isParseError
|
|
|
|
|
? 'Invalid response from provider'
|
|
|
|
|
: (error instanceof Error ? error.message : 'Request failed')
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
};
|