@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.
@@ -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',