@vibe-cafe/vibe-usage 0.10.33 → 0.11.0
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/README.md +14 -0
- package/package.json +1 -1
- package/src/daemon-service.js +4 -3
- package/src/index.js +19 -0
- package/src/parsers/claude-code.js +13 -0
- package/src/parsers/codebuddy.js +202 -0
- package/src/parsers/index.js +2 -0
- package/src/quotas/cache.js +91 -0
- package/src/quotas/index.js +35 -0
- package/src/quotas/providers/grok.js +151 -0
- package/src/quotas/providers/kimi-code.js +504 -0
- package/src/quotas/providers/zai.js +143 -0
- package/src/quotas/registry.js +114 -0
- package/src/quotas/schema.js +94 -0
- package/src/tools.js +13 -1
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { accessSync, constants, existsSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { delimiter, join } from 'node:path';
|
|
4
|
+
import { loadCachedQuota, saveCachedQuota } from './cache.js';
|
|
5
|
+
import { fetchGrokQuota } from './providers/grok.js';
|
|
6
|
+
import { fetchKimiCodeQuota } from './providers/kimi-code.js';
|
|
7
|
+
import { fetchZaiQuota } from './providers/zai.js';
|
|
8
|
+
import { FETCHABLE_QUOTA_PRODUCT_IDS, quotaEnvelope, quotaResult } from './schema.js';
|
|
9
|
+
|
|
10
|
+
const providers = new Map([
|
|
11
|
+
['kimi-code', fetchKimiCodeQuota],
|
|
12
|
+
['zcode', fetchZaiQuota],
|
|
13
|
+
['grok', fetchGrokQuota],
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
function executableExists(name, environment, platform) {
|
|
17
|
+
const pathDelimiter = platform === 'win32' ? ';' : delimiter;
|
|
18
|
+
const candidateNames = [name];
|
|
19
|
+
if (platform === 'win32') {
|
|
20
|
+
const extensions = (environment.PATHEXT || '.COM;.EXE;.BAT;.CMD')
|
|
21
|
+
.split(';')
|
|
22
|
+
.map(value => value.trim())
|
|
23
|
+
.filter(Boolean)
|
|
24
|
+
.map(value => value.startsWith('.') ? value : `.${value}`);
|
|
25
|
+
for (const extension of extensions) {
|
|
26
|
+
candidateNames.push(`${name}${extension}`, `${name}${extension.toLowerCase()}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return (environment.PATH || '').split(pathDelimiter).filter(Boolean).some(directory => (
|
|
31
|
+
candidateNames.some(candidate => {
|
|
32
|
+
const path = join(directory, candidate);
|
|
33
|
+
try {
|
|
34
|
+
// Windows does not expose POSIX execute bits; file presence plus a
|
|
35
|
+
// PATHEXT executable suffix is its ordinary command-discovery rule.
|
|
36
|
+
accessSync(path, platform === 'win32' ? constants.F_OK : constants.X_OK);
|
|
37
|
+
return true;
|
|
38
|
+
} catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function discoverQuotaProducts({
|
|
46
|
+
environment = process.env,
|
|
47
|
+
home = homedir(),
|
|
48
|
+
platform = process.platform,
|
|
49
|
+
} = {}) {
|
|
50
|
+
const applications = platform === 'darwin'
|
|
51
|
+
? ['/Applications', join(home, 'Applications')] : [];
|
|
52
|
+
const existsAny = paths => paths.some(path => existsSync(path));
|
|
53
|
+
const configuredGrokHome = environment.GROK_HOME?.trim();
|
|
54
|
+
const grokHome = configuredGrokHome
|
|
55
|
+
? configuredGrokHome.replace(/^~(?=$|[\\/])/, home)
|
|
56
|
+
: join(home, '.grok');
|
|
57
|
+
return quotaEnvelope([
|
|
58
|
+
{
|
|
59
|
+
id: 'kimi-code',
|
|
60
|
+
detected: existsAny([join(home, '.kimi'), join(home, '.kimi-code')])
|
|
61
|
+
|| executableExists('kimi', environment, platform),
|
|
62
|
+
fetchable: true,
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
id: 'zcode',
|
|
66
|
+
detected: existsAny([join(home, '.zcode'), join(home, '.config', 'zcode'),
|
|
67
|
+
...applications.map(path => join(path, 'ZCode.app'))])
|
|
68
|
+
|| executableExists('zcode', environment, platform),
|
|
69
|
+
fetchable: true,
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
id: 'grok',
|
|
73
|
+
detected: existsAny([grokHome]) || executableExists('grok', environment, platform),
|
|
74
|
+
fetchable: true,
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
id: 'cursor',
|
|
78
|
+
detected: existsAny([join(home, '.cursor'),
|
|
79
|
+
...applications.map(path => join(path, 'Cursor.app'))])
|
|
80
|
+
|| executableExists('cursor', environment, platform),
|
|
81
|
+
fetchable: false,
|
|
82
|
+
},
|
|
83
|
+
]);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function fetchQuotaProducts(ids, options = {}) {
|
|
87
|
+
const unique = [...new Set(ids)];
|
|
88
|
+
const invalid = unique.filter(id => !FETCHABLE_QUOTA_PRODUCT_IDS.includes(id));
|
|
89
|
+
if (invalid.length) throw new Error(`Unsupported quota product: ${invalid.join(', ')}`);
|
|
90
|
+
|
|
91
|
+
const fetched = await Promise.all(unique.map(async id => {
|
|
92
|
+
try {
|
|
93
|
+
return await providers.get(id)(options);
|
|
94
|
+
} catch {
|
|
95
|
+
return quotaResult({ id, status: 'retryable_error', message: 'Provider failed unexpectedly' });
|
|
96
|
+
}
|
|
97
|
+
}));
|
|
98
|
+
const results = fetched.map(result => {
|
|
99
|
+
if (result.status === 'ok') {
|
|
100
|
+
saveCachedQuota(result, result.cacheScope, options.environment);
|
|
101
|
+
return result;
|
|
102
|
+
}
|
|
103
|
+
if (result.status === 'retryable_error') {
|
|
104
|
+
return loadCachedQuota(
|
|
105
|
+
result.id,
|
|
106
|
+
result.cacheScope,
|
|
107
|
+
options.environment,
|
|
108
|
+
options.now || new Date()
|
|
109
|
+
) || result;
|
|
110
|
+
}
|
|
111
|
+
return result;
|
|
112
|
+
});
|
|
113
|
+
return quotaEnvelope(results);
|
|
114
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
export const QUOTA_SCHEMA_VERSION = 1;
|
|
2
|
+
|
|
3
|
+
export const QUOTA_PRODUCT_IDS = Object.freeze([
|
|
4
|
+
'kimi-code',
|
|
5
|
+
'zcode',
|
|
6
|
+
'grok',
|
|
7
|
+
'cursor',
|
|
8
|
+
]);
|
|
9
|
+
|
|
10
|
+
export const FETCHABLE_QUOTA_PRODUCT_IDS = Object.freeze([
|
|
11
|
+
'kimi-code',
|
|
12
|
+
'zcode',
|
|
13
|
+
'grok',
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
const FETCH_STATUSES = new Set([
|
|
17
|
+
'ok',
|
|
18
|
+
'no_data',
|
|
19
|
+
'missing_credentials',
|
|
20
|
+
'expired_credentials',
|
|
21
|
+
'unauthorized',
|
|
22
|
+
'retryable_error',
|
|
23
|
+
'unsupported',
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
function finiteNumber(value, name) {
|
|
27
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
28
|
+
throw new TypeError(`${name} must be a finite number`);
|
|
29
|
+
}
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function optionalISODate(value, name) {
|
|
34
|
+
if (value === undefined || value === null) return undefined;
|
|
35
|
+
const date = value instanceof Date ? value : new Date(value);
|
|
36
|
+
if (Number.isNaN(date.getTime())) {
|
|
37
|
+
throw new TypeError(`${name} must be an ISO date string`);
|
|
38
|
+
}
|
|
39
|
+
return date.toISOString();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function normalizeMeter(raw, index = 0) {
|
|
43
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
44
|
+
throw new TypeError(`meters[${index}] must be an object`);
|
|
45
|
+
}
|
|
46
|
+
const id = String(raw.id || '').trim();
|
|
47
|
+
const label = String(raw.label || '').trim();
|
|
48
|
+
if (!id || !label) throw new TypeError(`meters[${index}] needs id and label`);
|
|
49
|
+
|
|
50
|
+
const utilization = Math.max(0, Math.min(100,
|
|
51
|
+
finiteNumber(raw.utilization, `meters[${index}].utilization`)));
|
|
52
|
+
const meter = { id, label, utilization };
|
|
53
|
+
const resetsAt = optionalISODate(raw.resetsAt, `meters[${index}].resetsAt`);
|
|
54
|
+
if (resetsAt) meter.resetsAt = resetsAt;
|
|
55
|
+
if (raw.windowSeconds !== undefined && raw.windowSeconds !== null) {
|
|
56
|
+
const seconds = finiteNumber(raw.windowSeconds, `meters[${index}].windowSeconds`);
|
|
57
|
+
if (seconds > 0) meter.windowSeconds = seconds;
|
|
58
|
+
}
|
|
59
|
+
return meter;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function quotaResult({
|
|
63
|
+
id,
|
|
64
|
+
status,
|
|
65
|
+
meters = [],
|
|
66
|
+
planLabel,
|
|
67
|
+
fetchedAt = new Date(),
|
|
68
|
+
dataAsOf = fetchedAt,
|
|
69
|
+
message,
|
|
70
|
+
source = 'live',
|
|
71
|
+
}) {
|
|
72
|
+
if (!FETCHABLE_QUOTA_PRODUCT_IDS.includes(id)) {
|
|
73
|
+
throw new TypeError(`unsupported quota product: ${id}`);
|
|
74
|
+
}
|
|
75
|
+
if (!FETCH_STATUSES.has(status)) {
|
|
76
|
+
throw new TypeError(`invalid quota status: ${status}`);
|
|
77
|
+
}
|
|
78
|
+
const result = {
|
|
79
|
+
id,
|
|
80
|
+
status,
|
|
81
|
+
meters: meters.map(normalizeMeter),
|
|
82
|
+
fetchedAt: new Date(fetchedAt).toISOString(),
|
|
83
|
+
source,
|
|
84
|
+
};
|
|
85
|
+
const normalizedDataAsOf = optionalISODate(dataAsOf, 'dataAsOf');
|
|
86
|
+
if (normalizedDataAsOf) result.dataAsOf = normalizedDataAsOf;
|
|
87
|
+
if (typeof planLabel === 'string' && planLabel.trim()) result.planLabel = planLabel.trim();
|
|
88
|
+
if (typeof message === 'string' && message.trim()) result.message = message.trim();
|
|
89
|
+
return result;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function quotaEnvelope(products) {
|
|
93
|
+
return { schemaVersion: QUOTA_SCHEMA_VERSION, products };
|
|
94
|
+
}
|
package/src/tools.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { existsSync, readdirSync, statSync } from 'node:fs';
|
|
2
|
-
import { dirname, isAbsolute, join, posix, resolve, win32 } from 'node:path';
|
|
2
|
+
import { delimiter, dirname, isAbsolute, join, posix, resolve, win32 } from 'node:path';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { getOpenCodeStores } from './opencode-roots.js';
|
|
5
5
|
import { findClaudeCodeDataDirs } from './claude-roots.js';
|
|
@@ -182,6 +182,12 @@ export function getMimocodeDbPath(env = process.env) {
|
|
|
182
182
|
return isAbsolute(env.MIMOCODE_DB) ? env.MIMOCODE_DB : join(dataDir, env.MIMOCODE_DB);
|
|
183
183
|
}
|
|
184
184
|
|
|
185
|
+
export function getCodebuddyRoots(env = process.env, home = homedir()) {
|
|
186
|
+
const override = env.VIBE_USAGE_CODEBUDDY_DIRS?.trim();
|
|
187
|
+
if (override) return override.split(delimiter).map(value => value.trim()).filter(Boolean);
|
|
188
|
+
return [env.CODEBUDDY_CONFIG_DIR?.trim() || join(home, '.codebuddy')];
|
|
189
|
+
}
|
|
190
|
+
|
|
185
191
|
export function getZcodeDbPath(env = process.env, home = homedir()) {
|
|
186
192
|
const override = env.VIBE_USAGE_ZCODE_DB?.trim();
|
|
187
193
|
if (override) return isAbsolute(override) ? override : resolve(override);
|
|
@@ -451,6 +457,12 @@ export const TOOLS = [
|
|
|
451
457
|
id: 'zcode',
|
|
452
458
|
dataDir: getZcodeDbPath(),
|
|
453
459
|
},
|
|
460
|
+
{
|
|
461
|
+
name: 'CodeBuddy',
|
|
462
|
+
id: 'codebuddy',
|
|
463
|
+
dataDir: join(getCodebuddyRoots()[0], 'projects'),
|
|
464
|
+
detectDataDirs: () => getCodebuddyRoots().map(root => join(root, 'projects')).filter(existsSync),
|
|
465
|
+
},
|
|
454
466
|
{
|
|
455
467
|
name: 'Devin',
|
|
456
468
|
id: 'devin',
|