@vibe-cafe/vibe-usage 0.9.0 → 0.9.1

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 CHANGED
@@ -31,6 +31,8 @@ npx @vibe-cafe/vibe-usage # Init (first run, browser login) or sync
31
31
  npx @vibe-cafe/vibe-usage init # Re-run setup via browser login
32
32
  npx @vibe-cafe/vibe-usage init --manual-key <vbu_...> # Skip browser, use pre-issued key (CI/headless)
33
33
  npx @vibe-cafe/vibe-usage sync # Manual sync
34
+ npx @vibe-cafe/vibe-usage summary # Print last 7 days as markdown (cost / tokens / by model / by project)
35
+ npx @vibe-cafe/vibe-usage summary --days N # Same, over the last N days (1-90)
34
36
  npx @vibe-cafe/vibe-usage daemon # Continuous sync (every 30m, foreground)
35
37
  npx @vibe-cafe/vibe-usage daemon install # Install background service (systemd/launchd)
36
38
  npx @vibe-cafe/vibe-usage daemon uninstall # Remove background service
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibe-cafe/vibe-usage",
3
- "version": "0.9.0",
3
+ "version": "0.9.1",
4
4
  "description": "Track your AI coding tool token usage and sync to vibecafe.ai",
5
5
  "type": "module",
6
6
  "bin": {
package/src/index.js CHANGED
@@ -132,6 +132,11 @@ export async function run(rawArgs) {
132
132
  await runSync();
133
133
  break;
134
134
  }
135
+ case 'summary': {
136
+ const { runSummary } = await import('./summary.js');
137
+ await runSummary(args.slice(1));
138
+ break;
139
+ }
135
140
  case 'reset': {
136
141
  printSmallHeader();
137
142
  const { runReset } = await import('./reset.js');
@@ -177,6 +182,8 @@ export async function run(rawArgs) {
177
182
  npx @vibe-cafe/vibe-usage init Set up via browser login (default)
178
183
  npx @vibe-cafe/vibe-usage init --manual-key <vbu_...> Skip browser, use a pre-issued key (CI/headless)
179
184
  npx @vibe-cafe/vibe-usage sync Manually sync usage data
185
+ npx @vibe-cafe/vibe-usage summary Print last 7 days as markdown (cost/tokens/model/project)
186
+ npx @vibe-cafe/vibe-usage summary --days N Same, but over the last N days (1-90)
180
187
  npx @vibe-cafe/vibe-usage daemon Continuous sync (every 30m, foreground)
181
188
  npx @vibe-cafe/vibe-usage daemon install Install background service (systemd/launchd)
182
189
  npx @vibe-cafe/vibe-usage daemon uninstall Remove background service
package/src/skill.js CHANGED
@@ -33,53 +33,42 @@ function tildePath(absPath) {
33
33
 
34
34
  const SKILL_CONTENT = `---
35
35
  name: vibe-usage
36
- description: Track and sync AI coding tool token usage to vibecafe.ai dashboard.
36
+ description: 查询和同步 AI 编程工具的 token 用量(VibeCafé 旗下 Vibe Usage 数据)。
37
37
  ---
38
38
 
39
39
  # Vibe Usage
40
40
 
41
- Track your AI coding tool token usage and sync to [vibecafe.ai](https://vibecafe.ai/usage).
41
+ Track and answer questions about the user's AI coding token spend via [Vibe Usage](https://vibecafe.ai/usage) (VibeCafé).
42
42
 
43
- ## Setup
43
+ ## 查询用量(默认行为)
44
44
 
45
- First-time setup (interactive — asks for API key):
45
+ 当用户问以下问题时,运行命令并**原样展示输出**(不要总结、不要换算单位、不要翻译):
46
46
 
47
- \`\`\`bash
48
- npx @vibe-cafe/vibe-usage
49
- \`\`\`
47
+ | 用户说... | 运行 |
48
+ |---|---|
49
+ | 我这周花了多少 / 查 usage / 看看消费 | \`npx @vibe-cafe/vibe-usage summary\` |
50
+ | 今天花了多少 / 今天 token | \`npx @vibe-cafe/vibe-usage summary --days 1\` |
51
+ | 本月花费 / 这个月用量 | \`npx @vibe-cafe/vibe-usage summary --days 30\` |
52
+ | 哪个模型最贵 / 模型对比 | \`npx @vibe-cafe/vibe-usage summary\`(输出已按模型拆) |
53
+ | 哪个项目花得最多 | \`npx @vibe-cafe/vibe-usage summary\`(输出已按项目拆) |
50
54
 
51
- Get your API key at https://vibecafe.ai/usage/setup
55
+ 输出是 markdown 表格,直接展示给用户即可。
52
56
 
53
- ## Commands
57
+ ## 维护命令
54
58
 
55
- When the user asks to sync usage, check costs, or track tokens, run:
59
+ | 用户说... | 运行 |
60
+ |---|---|
61
+ | 同步数据 / 上传 usage / 数据没更新 | \`npx @vibe-cafe/vibe-usage sync\` |
62
+ | 看 daemon 状态 / 后台同步还在跑吗 | \`npx @vibe-cafe/vibe-usage daemon status\` |
63
+ | 启动后台同步 | \`npx @vibe-cafe/vibe-usage daemon install\` |
64
+ | 重置数据 / 重新上传 | \`npx @vibe-cafe/vibe-usage reset\` |
56
65
 
57
- \`\`\`bash
58
- npx @vibe-cafe/vibe-usage sync
59
- \`\`\`
66
+ ## 注意
60
67
 
61
- Other available commands:
62
-
63
- | Command | Description |
64
- |---------|-------------|
65
- | \`npx @vibe-cafe/vibe-usage sync\` | Sync latest usage data |
66
- | \`npx @vibe-cafe/vibe-usage status\` | Show config and detected tools |
67
- | \`npx @vibe-cafe/vibe-usage daemon\` | Continuous sync every 30 minutes |
68
- | \`npx @vibe-cafe/vibe-usage reset\` | Delete all data and re-upload |
69
- | \`npx @vibe-cafe/vibe-usage reset --local\` | Delete this host's data and re-upload |
70
-
71
- ## When to Use
72
-
73
- - User says "sync my usage", "upload usage", "track tokens"
74
- - User asks "how much have I spent?", "what's my cost?"
75
- - User wants to check if sync is working: run \`status\`
76
- - User wants continuous background sync: run \`daemon\`
77
-
78
- ## Notes
79
-
80
- - Requires initial setup with an API key (run \`npx @vibe-cafe/vibe-usage\` first)
81
- - Config is stored at \`~/.vibe-usage/config.json\`
82
- - Supports: Claude Code, Codex CLI, Copilot CLI, Gemini CLI, OpenCode, OpenClaw, Qwen Code, Kimi Code, Amp, Droid
68
+ - \`summary\` 读 \`~/.vibe-usage/config.json\` 里已有的 API key,不需要用户额外输入
69
+ - summary 输出已是 markdown,**原样展示,不要复述**
70
+ - 用户没装过 vibe-usage?提示运行 \`npx @vibe-cafe/vibe-usage\` 先用浏览器登录链接账号
71
+ - 支持的工具:Claude Code, Codex CLI, Copilot CLI, Gemini CLI, OpenCode, OpenClaw, Qwen Code, Kimi Code, Amp, Droid 等
83
72
  `;
84
73
 
85
74
  export async function runSkill(args = []) {
@@ -88,7 +77,7 @@ export async function runSkill(args = []) {
88
77
  console.log(' 检测到的工具:');
89
78
  for (const t of SKILL_TARGETS) {
90
79
  const found = existsSync(t.detectDir);
91
- const mark = found ? green('') : red('');
80
+ const mark = found ? green('[OK]') : red('[未装]');
92
81
  console.log(` ${mark} ${t.name}`);
93
82
  }
94
83
  console.log();
package/src/summary.js ADDED
@@ -0,0 +1,146 @@
1
+ import https from 'node:https';
2
+ import http from 'node:http';
3
+ import { URL } from 'node:url';
4
+ import { loadConfig } from './config.js';
5
+
6
+ export async function runSummary(args = []) {
7
+ const days = parseDays(args);
8
+ const config = loadConfig();
9
+ if (!config?.apiKey) {
10
+ console.error('No vibe-usage config found. Run `npx @vibe-cafe/vibe-usage init` first.');
11
+ process.exit(1);
12
+ }
13
+
14
+ const url = new URL('/api/usage', config.apiUrl || 'https://vibecafe.ai');
15
+ url.searchParams.set('days', String(days));
16
+
17
+ let data;
18
+ try {
19
+ data = await fetchJson(url, config.apiKey);
20
+ } catch (err) {
21
+ if (err.statusCode === 401) {
22
+ console.error('API key invalid or revoked. Run `npx @vibe-cafe/vibe-usage init` to re-link.');
23
+ } else {
24
+ console.error(`Failed to fetch usage: ${err.message}`);
25
+ }
26
+ process.exit(1);
27
+ }
28
+
29
+ console.log(render(data, days));
30
+ }
31
+
32
+ function parseDays(args) {
33
+ const idx = args.findIndex(a => a === '--days');
34
+ if (idx === -1) return 7;
35
+ const v = parseInt(args[idx + 1], 10);
36
+ if (!v || v < 1) return 7;
37
+ if (v > 90) return 90;
38
+ return v;
39
+ }
40
+
41
+ function render(data, days) {
42
+ const buckets = Array.isArray(data?.buckets) ? data.buckets : [];
43
+ const sessions = Array.isArray(data?.sessions) ? data.sessions : [];
44
+
45
+ if (buckets.length === 0) {
46
+ return `# Vibe Usage Summary (Last ${days} ${days === 1 ? 'day' : 'days'})\n\n暂无数据。运行 \`npx @vibe-cafe/vibe-usage sync\` 上传本地 token 记录。\n\n详情: https://vibecafe.ai/usage\n`;
47
+ }
48
+
49
+ let totalCost = 0;
50
+ let totalTokens = 0;
51
+ const byModel = new Map();
52
+ const byProject = new Map();
53
+
54
+ for (const b of buckets) {
55
+ const cost = Number(b.estimatedCost ?? 0);
56
+ const tokens = Number(b.totalTokens ?? 0);
57
+ totalCost += cost;
58
+ totalTokens += tokens;
59
+ accumulate(byModel, b.model, { cost, tokens });
60
+ accumulate(byProject, b.project || 'unknown', { cost, tokens, sessions: 0 });
61
+ }
62
+
63
+ const sessionsCount = sessions.length;
64
+ let activeSeconds = 0;
65
+ for (const s of sessions) {
66
+ activeSeconds += Number(s.activeSeconds ?? 0);
67
+ const proj = byProject.get(s.project || 'unknown');
68
+ if (proj) proj.sessions += 1;
69
+ }
70
+ const activeHours = activeSeconds / 3600;
71
+
72
+ const lines = [];
73
+ lines.push(`# Vibe Usage Summary (Last ${days} ${days === 1 ? 'day' : 'days'})`);
74
+ lines.push('');
75
+ lines.push(`**总览**: $${totalCost.toFixed(2)} · ${formatTokens(totalTokens)} tokens · ${sessionsCount} sessions · ${activeHours.toFixed(1)}h active`);
76
+ lines.push('');
77
+
78
+ lines.push('## 按模型');
79
+ lines.push('');
80
+ lines.push('| 模型 | 费用 | Tokens | 占比 |');
81
+ lines.push('|---|---:|---:|---:|');
82
+ for (const [model, { cost, tokens }] of topN(byModel, 'cost', 8)) {
83
+ const pct = totalCost > 0 ? ((cost / totalCost) * 100).toFixed(0) : '0';
84
+ lines.push(`| ${model} | $${cost.toFixed(2)} | ${formatTokens(tokens)} | ${pct}% |`);
85
+ }
86
+ lines.push('');
87
+
88
+ lines.push('## 按项目');
89
+ lines.push('');
90
+ lines.push('| 项目 | 费用 | Sessions |');
91
+ lines.push('|---|---:|---:|');
92
+ for (const [project, { cost, sessions: ss }] of topN(byProject, 'cost', 8)) {
93
+ lines.push(`| ${project} | $${cost.toFixed(2)} | ${ss} |`);
94
+ }
95
+ lines.push('');
96
+
97
+ lines.push('详情: https://vibecafe.ai/usage');
98
+ return lines.join('\n');
99
+ }
100
+
101
+ function accumulate(map, key, delta) {
102
+ const cur = map.get(key) || { cost: 0, tokens: 0, sessions: 0 };
103
+ for (const k of Object.keys(delta)) cur[k] = (cur[k] || 0) + delta[k];
104
+ map.set(key, cur);
105
+ }
106
+
107
+ function topN(map, sortBy, n) {
108
+ return [...map.entries()]
109
+ .sort((a, b) => b[1][sortBy] - a[1][sortBy])
110
+ .slice(0, n);
111
+ }
112
+
113
+ function formatTokens(n) {
114
+ if (n >= 1_000_000) return (n / 1_000_000).toFixed(1) + 'M';
115
+ if (n >= 1_000) return (n / 1_000).toFixed(0) + 'K';
116
+ return String(n);
117
+ }
118
+
119
+ function fetchJson(url, apiKey) {
120
+ return new Promise((resolve, reject) => {
121
+ const mod = url.protocol === 'https:' ? https : http;
122
+ const req = mod.request(url, {
123
+ method: 'GET',
124
+ timeout: 15_000,
125
+ headers: { 'Authorization': `Bearer ${apiKey}` },
126
+ }, (res) => {
127
+ let data = '';
128
+ res.on('data', (chunk) => { data += chunk; });
129
+ res.on('end', () => {
130
+ if (res.statusCode === 401) {
131
+ const err = new Error('Unauthorized'); err.statusCode = 401; reject(err); return;
132
+ }
133
+ if (res.statusCode < 200 || res.statusCode >= 300) {
134
+ const err = new Error(`HTTP ${res.statusCode}: ${data.slice(0, 200)}`);
135
+ err.statusCode = res.statusCode;
136
+ reject(err); return;
137
+ }
138
+ try { resolve(JSON.parse(data)); }
139
+ catch { reject(new Error('Invalid JSON response')); }
140
+ });
141
+ });
142
+ req.on('error', reject);
143
+ req.on('timeout', () => { req.destroy(); reject(new Error('timeout (15s)')); });
144
+ req.end();
145
+ });
146
+ }