@ugurcandede/cc-cost 0.1.0 → 0.1.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 +3 -2
- package/dist/cli.js +42 -6
- package/dist/i18n.js +24 -2
- package/dist/update.js +61 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
<h1>cc-cost</h1>
|
|
4
4
|
<p>What your Claude Code usage would cost at API list prices, totalled across all your machines (Windows, Mac, Linux), with an archive that outlives Claude Code's transcript cleanup and a dashboard you open with a double-click.</p>
|
|
5
5
|
<br>
|
|
6
|
-
<a href="https://
|
|
6
|
+
<a href="https://www.npmjs.com/package/@ugurcandede/cc-cost" target="_blank"><img src="https://img.shields.io/npm/v/@ugurcandede/cc-cost?style=flat-square&label=version" alt="npm version"></a>
|
|
7
7
|
<img src="https://img.shields.io/badge/Node.js-22%2B%20·%20zero%20dependencies-339933?style=flat-square&logo=node.js&logoColor=white" alt="Node.js 22+, zero dependencies">
|
|
8
8
|
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue?style=flat-square" alt="License: MIT"></a>
|
|
9
9
|
<br>
|
|
@@ -32,7 +32,7 @@ npm i -g @ugurcandede/cc-cost # or, with Yarn 1: yarn global add @ugurc
|
|
|
32
32
|
cc-cost setup
|
|
33
33
|
|
|
34
34
|
cc-cost # sync this machine, print a summary
|
|
35
|
-
cc-cost report
|
|
35
|
+
cc-cost report # open the dashboard
|
|
36
36
|
```
|
|
37
37
|
|
|
38
38
|
Run `setup` on every machine and point them at the same folder. Each one adds its numbers; every one
|
|
@@ -59,6 +59,7 @@ cc-cost insights # context size, cache efficiency, what drives the c
|
|
|
59
59
|
cc-cost plan # API equivalent vs Pro / Max 5x / Max 20x, rate-limit hits
|
|
60
60
|
cc-cost blocks # usage in Claude's 5-hour windows
|
|
61
61
|
cc-cost status # settings, machines, schedule, hook
|
|
62
|
+
cc-cost update # update to the latest version
|
|
62
63
|
```
|
|
63
64
|
|
|
64
65
|
```
|
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { spawn } from 'node:child_process';
|
|
2
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import { createRequire } from 'node:module';
|
|
5
5
|
import path from 'node:path';
|
|
@@ -16,6 +16,7 @@ import { blocksReport, dimensionJson, dimensionReport, DIMENSIONS, insights, lim
|
|
|
16
16
|
import { localClock } from "./scan.js";
|
|
17
17
|
import { claudeSettingsFile, currentRunner, dataFolderIn, hookInstalled, installHook, installSchedule, isEphemeral, removeHook, removeSchedule, SCHEDULE_TIME, scheduleInstalled, syncFolders, } from "./setup.js";
|
|
18
18
|
import { loadAll } from "./snapshot.js";
|
|
19
|
+
import { installMethod, isNewer, latestVersion, updateCommand } from "./update.js";
|
|
19
20
|
import { dashboardPath, sync } from "./sync.js";
|
|
20
21
|
// Name, version and project page come from package.json only
|
|
21
22
|
const pkg = createRequire(import.meta.url)('../package.json');
|
|
@@ -44,7 +45,7 @@ const { values: opt, positionals } = parseArgs({
|
|
|
44
45
|
'no-sync': { type: 'boolean' },
|
|
45
46
|
offline: { type: 'boolean' },
|
|
46
47
|
quiet: { type: 'boolean' },
|
|
47
|
-
|
|
48
|
+
location: { type: 'boolean' },
|
|
48
49
|
refresh: { type: 'boolean' },
|
|
49
50
|
'sync-dir': { type: 'string' },
|
|
50
51
|
yes: { type: 'boolean', short: 'y' },
|
|
@@ -109,9 +110,11 @@ function openFile(file) {
|
|
|
109
110
|
const [cmd, args] = process.platform === 'win32' ? ['cmd', ['/c', 'start', '""', file]]
|
|
110
111
|
: process.platform === 'darwin' ? ['open', [file]]
|
|
111
112
|
: ['xdg-open', [file]];
|
|
112
|
-
|
|
113
|
+
// No browser opener (a server, SSH): the path printed alongside is enough, so ignore the failure.
|
|
114
|
+
spawn(cmd, args, { detached: true, stdio: 'ignore' }).on('error', () => { }).unref();
|
|
113
115
|
}
|
|
114
116
|
const pricesCache = () => path.join(configDir(), 'pricing-cache.json');
|
|
117
|
+
const updateCache = () => path.join(configDir(), 'update-check.json');
|
|
115
118
|
async function data(verbose) {
|
|
116
119
|
if (!opt['no-sync']) {
|
|
117
120
|
const r = await sync(settings);
|
|
@@ -142,11 +145,42 @@ function dashboard(d) {
|
|
|
142
145
|
site: pkg.author.url,
|
|
143
146
|
}, dashboardPath(settings));
|
|
144
147
|
}
|
|
145
|
-
function footer(d) {
|
|
148
|
+
async function footer(d) {
|
|
146
149
|
if (d.prices.note)
|
|
147
150
|
out(fill(L().pricesFallback, { reason: d.prices.note, source: d.prices.table.source, date: d.prices.table.date.slice(0, 10) }));
|
|
148
151
|
if (d.unknown.size)
|
|
149
152
|
out(fill(L().unknownModels, { list: [...d.unknown].join(', ') }));
|
|
153
|
+
// At most one registry check a day, and never in machine-readable or offline output
|
|
154
|
+
if (opt.json || opt.csv || opt.offline || opt.quiet)
|
|
155
|
+
return;
|
|
156
|
+
const latest = await latestVersion(pkg.name, updateCache());
|
|
157
|
+
if (latest && isNewer(latest, pkg.version))
|
|
158
|
+
out('\n' + paint('yellow', fill(L().update.available, { latest, current: pkg.version })));
|
|
159
|
+
}
|
|
160
|
+
async function update() {
|
|
161
|
+
const U = L().update;
|
|
162
|
+
const script = currentRunner().script;
|
|
163
|
+
const method = installMethod(script);
|
|
164
|
+
if (method === 'ephemeral')
|
|
165
|
+
return out(fill(U.ephemeral, { pkg: pkg.name }));
|
|
166
|
+
if (method === 'unknown')
|
|
167
|
+
return out(fill(U.manual, { path: script }));
|
|
168
|
+
let latest;
|
|
169
|
+
try {
|
|
170
|
+
latest = await latestVersion(pkg.name, updateCache(), { force: true });
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
fail(U.checkFailed);
|
|
174
|
+
}
|
|
175
|
+
if (!latest || !isNewer(latest, pkg.version))
|
|
176
|
+
return out(fill(U.upToDate, { current: pkg.version }));
|
|
177
|
+
const command = updateCommand[method](pkg.name).join(' ');
|
|
178
|
+
out(fill(U.running, { latest, command }));
|
|
179
|
+
// a fixed command line, no user input; the shell finds npm.cmd / yarn.cmd on Windows
|
|
180
|
+
const res = spawnSync(command, { stdio: 'inherit', shell: true });
|
|
181
|
+
if (res.status !== 0)
|
|
182
|
+
fail(fill(U.failed, { code: String(res.status), command }));
|
|
183
|
+
out(fill(U.done, { latest }));
|
|
150
184
|
}
|
|
151
185
|
// Print a table report as a table, CSV or JSON
|
|
152
186
|
function emit(report, json, empty) {
|
|
@@ -307,13 +341,13 @@ async function main() {
|
|
|
307
341
|
out(opt.json ? JSON.stringify(r.json, null, 2) : rows.length ? r.text : L().noData);
|
|
308
342
|
}
|
|
309
343
|
if (!opt.json && !opt.csv)
|
|
310
|
-
footer(d);
|
|
344
|
+
await footer(d);
|
|
311
345
|
return;
|
|
312
346
|
}
|
|
313
347
|
if (cmd === 'report') {
|
|
314
348
|
const file = dashboard(await data(false));
|
|
315
349
|
out(fill(L().dashboard, { file }));
|
|
316
|
-
if (opt.
|
|
350
|
+
if (!opt.location)
|
|
317
351
|
openFile(file);
|
|
318
352
|
return;
|
|
319
353
|
}
|
|
@@ -333,6 +367,8 @@ async function main() {
|
|
|
333
367
|
return setup();
|
|
334
368
|
if (cmd === 'status')
|
|
335
369
|
return status();
|
|
370
|
+
if (cmd === 'update')
|
|
371
|
+
return update();
|
|
336
372
|
if (cmd === 'config') {
|
|
337
373
|
if (rest[0] === 'set') {
|
|
338
374
|
const [key, value = ''] = rest.slice(1);
|
package/dist/i18n.js
CHANGED
|
@@ -14,10 +14,11 @@ Commands:
|
|
|
14
14
|
blocks Usage in 5-hour windows
|
|
15
15
|
insights Where the money goes and what drives it
|
|
16
16
|
plan API equivalent vs subscription prices, rate-limit hits
|
|
17
|
-
report
|
|
17
|
+
report Open the HTML dashboard (--location prints its path instead)
|
|
18
18
|
pricing Prices in use (--refresh to fetch them again)
|
|
19
19
|
setup Pick the shared folder, schedule daily runs, add the Claude Code hook
|
|
20
20
|
status Settings, machines, last sync, scheduler and hook
|
|
21
|
+
update Update cc-cost to the latest version
|
|
21
22
|
config Show settings; "config set <key> <value>" changes one
|
|
22
23
|
|
|
23
24
|
Filters:
|
|
@@ -126,6 +127,16 @@ Docs and issues: {url}`,
|
|
|
126
127
|
blocks: {
|
|
127
128
|
active: 'active, {left} left',
|
|
128
129
|
},
|
|
130
|
+
update: {
|
|
131
|
+
available: 'cc-cost {latest} is available (you have {current}). Run: cc-cost update',
|
|
132
|
+
upToDate: 'cc-cost {current} is up to date.',
|
|
133
|
+
running: 'Updating to {latest}: {command}',
|
|
134
|
+
done: 'Updated to cc-cost {latest}. The schedule and hook keep working; no need to run setup again.',
|
|
135
|
+
failed: 'The update failed (exit code {code}). Run it yourself: {command}',
|
|
136
|
+
checkFailed: 'Could not reach the npm registry to check for a newer version.',
|
|
137
|
+
ephemeral: 'cc-cost is running through npx or dlx, which fetch it each time. For the newest version: npx {pkg}@latest',
|
|
138
|
+
manual: 'cc-cost runs from {path}, not from a package-manager install. Update it the way you installed it (a git checkout: git pull, then yarn build).',
|
|
139
|
+
},
|
|
129
140
|
setup: {
|
|
130
141
|
title: 'cc-cost {version} setup',
|
|
131
142
|
found: 'Shared folders found: {list}',
|
|
@@ -216,10 +227,11 @@ Komutlar:
|
|
|
216
227
|
blocks 5 saatlik pencerelerde kullanım
|
|
217
228
|
insights Para nereye gidiyor, neyden kaynaklanıyor
|
|
218
229
|
plan API karşılığı ve abonelik fiyatları, limit aşımları
|
|
219
|
-
report HTML dashboard'u
|
|
230
|
+
report HTML dashboard'u aç (--location sadece yolunu basar)
|
|
220
231
|
pricing Kullanılan fiyatlar (--refresh ile yeniden çek)
|
|
221
232
|
setup Paylaşılan klasörü seç, günlük çalışmayı zamanla, Claude Code hook'unu ekle
|
|
222
233
|
status Ayarlar, makineler, son senkron, zamanlayıcı ve hook
|
|
234
|
+
update cc-cost'u en son sürüme güncelle
|
|
223
235
|
config Ayarları göster; "config set <anahtar> <değer>" ile değiştir
|
|
224
236
|
|
|
225
237
|
Filtreler:
|
|
@@ -328,6 +340,16 @@ Dokümantasyon ve hata bildirimi: {url}`,
|
|
|
328
340
|
blocks: {
|
|
329
341
|
active: 'aktif, {left} kaldı',
|
|
330
342
|
},
|
|
343
|
+
update: {
|
|
344
|
+
available: 'cc-cost {latest} yayında (sizdeki {current}). Güncellemek için: cc-cost update',
|
|
345
|
+
upToDate: 'cc-cost {current} güncel.',
|
|
346
|
+
running: '{latest} sürümüne güncelleniyor: {command}',
|
|
347
|
+
done: 'cc-cost {latest} sürümüne güncellendi. Zamanlama ve hook çalışmaya devam eder; setup\'ı yeniden çalıştırmaya gerek yok.',
|
|
348
|
+
failed: 'Güncelleme başarısız oldu (çıkış kodu {code}). Kendiniz çalıştırın: {command}',
|
|
349
|
+
checkFailed: 'Yeni sürümü kontrol etmek için npm registry\'ye ulaşılamadı.',
|
|
350
|
+
ephemeral: 'cc-cost npx ya da dlx ile çalışıyor; bunlar her seferinde paketi indirir. En yeni sürüm için: npx {pkg}@latest',
|
|
351
|
+
manual: 'cc-cost {path} konumundan çalışıyor, bir paket yöneticisi kurulumu değil. Nasıl kurduysanız öyle güncelleyin (git kopyası: git pull, ardından yarn build).',
|
|
352
|
+
},
|
|
331
353
|
setup: {
|
|
332
354
|
title: 'cc-cost {version} kurulumu',
|
|
333
355
|
found: 'Bulunan paylaşılan klasörler: {list}',
|
package/dist/update.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
const MAX_AGE_MS = 24 * 60 * 60 * 1000;
|
|
4
|
+
// x.y.z comparison; a pre-release (x.y.z-beta) never counts as newer than its release
|
|
5
|
+
export function isNewer(candidate, current) {
|
|
6
|
+
const parse = (v) => {
|
|
7
|
+
const [core, pre] = v.split('-');
|
|
8
|
+
return { n: (core ?? '').split('.').map((x) => Number(x) || 0), pre: !!pre };
|
|
9
|
+
};
|
|
10
|
+
const a = parse(candidate), b = parse(current);
|
|
11
|
+
for (let i = 0; i < 3; i++)
|
|
12
|
+
if ((a.n[i] ?? 0) !== (b.n[i] ?? 0))
|
|
13
|
+
return (a.n[i] ?? 0) > (b.n[i] ?? 0);
|
|
14
|
+
return !a.pre && b.pre;
|
|
15
|
+
}
|
|
16
|
+
// Latest published version, asked of the registry at most once a day unless `force`.
|
|
17
|
+
export async function latestVersion(name, cacheFile, opts = {}) {
|
|
18
|
+
let cached;
|
|
19
|
+
try {
|
|
20
|
+
cached = JSON.parse(fs.readFileSync(cacheFile, 'utf8'));
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
// first check
|
|
24
|
+
}
|
|
25
|
+
const fresh = cached && Date.now() - Date.parse(cached.checked) < MAX_AGE_MS;
|
|
26
|
+
if (opts.offline || (fresh && !opts.force))
|
|
27
|
+
return cached?.latest;
|
|
28
|
+
try {
|
|
29
|
+
const res = await fetch(`https://registry.npmjs.org/${name.replace('/', '%2f')}/latest`, { signal: AbortSignal.timeout(3000) });
|
|
30
|
+
if (!res.ok)
|
|
31
|
+
throw new Error(`HTTP ${res.status}`);
|
|
32
|
+
const latest = String((await res.json()).version ?? '');
|
|
33
|
+
if (!latest)
|
|
34
|
+
throw new Error('no version');
|
|
35
|
+
fs.mkdirSync(path.dirname(cacheFile), { recursive: true });
|
|
36
|
+
fs.writeFileSync(cacheFile, JSON.stringify({ checked: new Date().toISOString(), latest }));
|
|
37
|
+
return latest;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
if (opts.force)
|
|
41
|
+
throw new Error('registry unreachable');
|
|
42
|
+
return cached?.latest; // offline or registry down: say nothing new
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
// How this copy was installed, read from where its script lives
|
|
46
|
+
export function installMethod(script) {
|
|
47
|
+
if (/[\\/](_npx|dlx(-\d+)?)[\\/]/.test(script))
|
|
48
|
+
return 'ephemeral';
|
|
49
|
+
if (!/[\\/]node_modules[\\/]@ugurcandede[\\/]cc-cost[\\/]/.test(script))
|
|
50
|
+
return 'unknown'; // e.g. a git checkout
|
|
51
|
+
if (/[\\/]pnpm[\\/]/i.test(script))
|
|
52
|
+
return 'pnpm';
|
|
53
|
+
if (/[\\/]yarn[\\/](data[\\/])?global[\\/]/i.test(script))
|
|
54
|
+
return 'yarn';
|
|
55
|
+
return 'npm';
|
|
56
|
+
}
|
|
57
|
+
export const updateCommand = {
|
|
58
|
+
npm: (pkg) => ['npm', 'i', '-g', `${pkg}@latest`],
|
|
59
|
+
yarn: (pkg) => ['yarn', 'global', 'add', `${pkg}@latest`],
|
|
60
|
+
pnpm: (pkg) => ['pnpm', 'add', '-g', `${pkg}@latest`],
|
|
61
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ugurcandede/cc-cost",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "API-equivalent cost of your Claude Code usage across all your machines, with an archive that outlives transcript cleanup and an offline dashboard.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude",
|