tokenjam 0.5.3 → 0.5.5
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/bin/tj.js +76 -1
- package/package.json +13 -3
package/bin/tj.js
CHANGED
|
@@ -16,10 +16,27 @@
|
|
|
16
16
|
* 3. `tj …` — an already-installed CLI on PATH
|
|
17
17
|
*
|
|
18
18
|
* If none are present we print actionable install guidance and exit non-zero.
|
|
19
|
+
*
|
|
20
|
+
* Note: tokenjam >=0.5.4 also ships a `tokenjam` console-script alias
|
|
21
|
+
* alongside `tj`, so bare `uvx tokenjam` / `pipx run tokenjam` work too. This
|
|
22
|
+
* wrapper keeps the explicit `--from tokenjam tj` / `--spec tokenjam tj` form
|
|
23
|
+
* below for back-compat with the 0.5.3 and earlier releases it also targets.
|
|
24
|
+
*
|
|
25
|
+
* Freshness (issue #111): `uv` reuses its cached tool environment and never
|
|
26
|
+
* re-resolves on its own, so a machine that first ran this wrapper on an old
|
|
27
|
+
* release keeps getting that release forever, even after newer ones hit
|
|
28
|
+
* PyPI. To avoid pinning stale versions indefinitely, the `uvx` branch passes
|
|
29
|
+
* `--refresh` at most once per 24h (tracked via a timestamp file — see
|
|
30
|
+
* `shouldRefresh`/`markRefreshed` below). `pipx run` isn't touched: its own
|
|
31
|
+
* cache already expires after ~14 days on its own. The installed-`tj` branch
|
|
32
|
+
* has no cache to go stale.
|
|
19
33
|
*/
|
|
20
34
|
"use strict";
|
|
21
35
|
|
|
22
36
|
const { spawnSync } = require("child_process");
|
|
37
|
+
const fs = require("fs");
|
|
38
|
+
const os = require("os");
|
|
39
|
+
const path = require("path");
|
|
23
40
|
|
|
24
41
|
// PyPI package name vs. command name differ (`tokenjam` ships the `tj` script),
|
|
25
42
|
// so ephemeral runners must be told the source package explicitly.
|
|
@@ -40,6 +57,60 @@ function runners() {
|
|
|
40
57
|
];
|
|
41
58
|
}
|
|
42
59
|
|
|
60
|
+
// --- uvx cache freshness (issue #111) -------------------------------------
|
|
61
|
+
//
|
|
62
|
+
// Only the `uvx` runner needs this: `uv` caches a resolved tool environment
|
|
63
|
+
// and reuses it forever unless told to `--refresh`, so a returning user
|
|
64
|
+
// silently keeps whatever version they first resolved. Tracked with a plain
|
|
65
|
+
// timestamp file rather than anything fancier — this wrapper is intentionally
|
|
66
|
+
// dependency-free (stdlib `fs`/`os`/`path` only).
|
|
67
|
+
|
|
68
|
+
const REFRESH_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24h
|
|
69
|
+
|
|
70
|
+
function refreshCacheDir() {
|
|
71
|
+
const xdgCacheHome = process.env.XDG_CACHE_HOME;
|
|
72
|
+
const base =
|
|
73
|
+
xdgCacheHome && xdgCacheHome.trim()
|
|
74
|
+
? xdgCacheHome
|
|
75
|
+
: path.join(os.homedir(), ".cache");
|
|
76
|
+
return path.join(base, "tokenjam-npx");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function refreshTimestampPath() {
|
|
80
|
+
return path.join(refreshCacheDir(), "last-refresh");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// True if it's been >24h (or we've never refreshed / can't tell). Fails
|
|
84
|
+
// open on any fs error — an unwritable/unreadable cache dir must never
|
|
85
|
+
// break the wrapper, it just means we skip the freshness nudge this run.
|
|
86
|
+
function shouldRefresh() {
|
|
87
|
+
try {
|
|
88
|
+
const stat = fs.statSync(refreshTimestampPath());
|
|
89
|
+
return Date.now() - stat.mtimeMs > REFRESH_INTERVAL_MS;
|
|
90
|
+
} catch {
|
|
91
|
+
return true; // no timestamp yet (or unreadable) => treat as stale
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Called only AFTER `uvx --refresh` has actually returned with a zero exit
|
|
96
|
+
// status (not before we spawn it, and not on failure). If the refresh's
|
|
97
|
+
// download is interrupted partway through (network drop, Ctrl-C, OOM kill),
|
|
98
|
+
// spawnSync never returns normally, this never runs. If it returns but uv
|
|
99
|
+
// exits non-zero (PyPI unreachable, partial download), the caller also
|
|
100
|
+
// skips this call. Either way the *next* invocation still sees a
|
|
101
|
+
// stale/missing timestamp and retries `--refresh`. Writing the timestamp up
|
|
102
|
+
// front, or unconditionally on return, would mark the cache "fresh" even
|
|
103
|
+
// though that refresh never completed, silently pinning a broken/partial
|
|
104
|
+
// environment for a full 24h. Best-effort/fail-open: swallow fs errors.
|
|
105
|
+
function markRefreshed() {
|
|
106
|
+
try {
|
|
107
|
+
fs.mkdirSync(refreshCacheDir(), { recursive: true });
|
|
108
|
+
fs.writeFileSync(refreshTimestampPath(), String(Date.now()));
|
|
109
|
+
} catch {
|
|
110
|
+
// fail open — worst case we just try to refresh again next run
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
43
114
|
function main() {
|
|
44
115
|
// Bare `npx tokenjam` IS the zero-install first run — route it to
|
|
45
116
|
// `tj quickstart` (the quota report the docs promise). The branded home
|
|
@@ -51,10 +122,14 @@ function main() {
|
|
|
51
122
|
|
|
52
123
|
for (const { bin, prefix } of runners()) {
|
|
53
124
|
if (!has(bin)) continue;
|
|
54
|
-
const
|
|
125
|
+
const isUvx = bin === "uvx";
|
|
126
|
+
const doRefresh = isUvx && shouldRefresh();
|
|
127
|
+
const args = doRefresh ? ["--refresh", ...prefix] : prefix;
|
|
128
|
+
const result = spawnSync(bin, [...args, ...passthrough], {
|
|
55
129
|
stdio: "inherit",
|
|
56
130
|
});
|
|
57
131
|
if (result.error) continue; // try the next runner on spawn failure
|
|
132
|
+
if (doRefresh && result.status === 0) markRefreshed();
|
|
58
133
|
process.exit(result.status === null ? 1 : result.status);
|
|
59
134
|
}
|
|
60
135
|
|
package/package.json
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tokenjam",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.5",
|
|
4
4
|
"description": "Zero-install launcher for TokenJam (tj) — `npx tokenjam` runs the Python CLI via uvx/pipx and prints where your Claude Code quota actually goes, no setup.",
|
|
5
|
-
"keywords": [
|
|
5
|
+
"keywords": [
|
|
6
|
+
"claude-code",
|
|
7
|
+
"ccusage",
|
|
8
|
+
"tokens",
|
|
9
|
+
"cost",
|
|
10
|
+
"llm",
|
|
11
|
+
"agents",
|
|
12
|
+
"observability",
|
|
13
|
+
"tokenjam"
|
|
14
|
+
],
|
|
6
15
|
"homepage": "https://tokenjam.dev",
|
|
7
16
|
"bugs": "https://github.com/Metabuilder-Labs/tokenjam/issues",
|
|
8
17
|
"license": "MIT",
|
|
@@ -12,7 +21,8 @@
|
|
|
12
21
|
"directory": "npm-wrapper"
|
|
13
22
|
},
|
|
14
23
|
"bin": {
|
|
15
|
-
"tj": "bin/tj.js"
|
|
24
|
+
"tj": "bin/tj.js",
|
|
25
|
+
"tokenjam": "bin/tj.js"
|
|
16
26
|
},
|
|
17
27
|
"files": [
|
|
18
28
|
"bin/tj.js",
|