tokenjam 0.5.7 → 0.6.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 +6 -14
- package/bin/tj.js +198 -70
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -11,17 +11,15 @@
|
|
|
11
11
|
|
|
12
12
|
</div>
|
|
13
13
|
|
|
14
|
-
TokenJam ingests telemetry data about your agents from a multitude of sources and provides you a quick and easy way to visualize and optimize cost so that you get the most out of the tokens you pay for. This package is the zero-install launcher:
|
|
14
|
+
TokenJam ingests telemetry data about your agents from a multitude of sources and provides you a quick and easy way to visualize and optimize cost so that you get the most out of the tokens you pay for. This package is the zero-install launcher: no pip environment, no manual config.
|
|
15
15
|
|
|
16
16
|
```bash
|
|
17
|
-
npx tokenjam
|
|
17
|
+
npx tokenjam onboard # or: pipx install tokenjam && tj onboard
|
|
18
18
|
```
|
|
19
19
|
|
|
20
20
|
## What you get
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
For the full setup, run `npx tokenjam onboard`: it wires up live capture, all six analyzers, the Lens dashboard, and the zero-token statusline in one command.
|
|
22
|
+
`tj onboard` is guided setup: it writes a config, generates an ingest secret, and asks how you use AI agents (Claude Code, Codex, or your own SDK/API agents) to wire the right path. For Claude Code and Codex that means backfilling recent history and installing a statusline and hooks for live capture; restart and you're live. Onboarding unlocks all six analyzers, the Lens dashboard, and the zero-token statusline in one command.
|
|
25
23
|
|
|
26
24
|
## Commands
|
|
27
25
|
|
|
@@ -29,20 +27,14 @@ All arguments pass straight through to the Python CLI, so any `tj` subcommand an
|
|
|
29
27
|
|
|
30
28
|
| Command | What it does |
|
|
31
29
|
|---|---|
|
|
32
|
-
| `npx tokenjam` |
|
|
30
|
+
| `npx tokenjam onboard` | Guided setup: writes a config, generates an ingest secret, and optionally installs the background daemon for live capture. |
|
|
33
31
|
| `npx tokenjam context` | Where your quota goes: re-read vs. net-new share, recurring inclusions, `/compact` candidates. |
|
|
34
32
|
| `npx tokenjam optimize` | Cost-saving candidates: model downsizing, cache opportunities, prompt trimming, workflow reuse, subagent right-sizing. |
|
|
35
|
-
| `npx tokenjam
|
|
33
|
+
| `npx tokenjam` | Bare run: still works, still zero-install, still a reference passthrough to the Python CLI. |
|
|
36
34
|
|
|
37
35
|
## Go deeper
|
|
38
36
|
|
|
39
|
-
`
|
|
40
|
-
|
|
41
|
-
```bash
|
|
42
|
-
npx tokenjam onboard # or: pipx install tokenjam && tj onboard
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
`tj onboard` asks how you use AI agents (Claude Code, Codex, or your own SDK/API agents) and wires the right path. For Claude Code and Codex that means backfilling recent history plus a statusline and hooks; restart and you're live. From there:
|
|
37
|
+
`tj onboard` sets up live capture, the local Lens dashboard, and the zero-token statusline in one command. From there:
|
|
46
38
|
|
|
47
39
|
```bash
|
|
48
40
|
tj optimize # cost-saving candidates from your actual usage
|
package/bin/tj.js
CHANGED
|
@@ -7,12 +7,13 @@
|
|
|
7
7
|
* `npx <tool>` first. `npx tokenjam` here resolves a Python launcher with NO pip env,
|
|
8
8
|
* NO daemon, NO onboarding — it shells out to the Python CLI via the first
|
|
9
9
|
* available runner and hands every argument straight through. Bare `npx tokenjam`
|
|
10
|
-
* (no subcommand)
|
|
11
|
-
* from the same ~/.claude/projects/*.jsonl files ccusage reads, in one
|
|
10
|
+
* (no subcommand) prints the zero-install report: where your Claude Code quota
|
|
11
|
+
* goes, from the same ~/.claude/projects/*.jsonl files ccusage reads, in one
|
|
12
|
+
* command.
|
|
12
13
|
*
|
|
13
14
|
* Runner preference (first that exists wins):
|
|
14
|
-
* 1. `uvx --from tokenjam tj …` — fully ephemeral, downloads nothing global
|
|
15
|
-
* 2. `pipx run --spec tokenjam tj …`
|
|
15
|
+
* 1. `uvx --from tokenjam==<own version> tj …` — fully ephemeral, downloads nothing global
|
|
16
|
+
* 2. `pipx run --spec tokenjam==<own version> tj …`
|
|
16
17
|
* 3. `tj …` — an already-installed CLI on PATH
|
|
17
18
|
*
|
|
18
19
|
* If none are present we print actionable install guidance and exit non-zero.
|
|
@@ -22,20 +23,27 @@
|
|
|
22
23
|
* wrapper keeps the explicit `--from tokenjam tj` / `--spec tokenjam tj` form
|
|
23
24
|
* below for back-compat with the 0.5.3 and earlier releases it also targets.
|
|
24
25
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
26
|
+
* Version pinning: `uv`/`pipx` cache a resolved tool environment and reuse it
|
|
27
|
+
* forever unless the requested spec changes — an unpinned `--from tokenjam`
|
|
28
|
+
* silently keeps reusing whatever was resolved first (e.g. a prior `uv tool
|
|
29
|
+
* install tokenjam` at an old version), never re-resolving on its own, no
|
|
30
|
+
* matter how many newer releases hit PyPI since. Pinning `--from
|
|
31
|
+
* tokenjam==<version>` / `--spec tokenjam==<version>` to this wrapper's OWN
|
|
32
|
+
* version (kept in sync with the release tag by publish-npm.yml's `npm
|
|
33
|
+
* version ${GITHUB_REF_NAME#v}` step) forces the resolver past that shortcut,
|
|
34
|
+
* so `npx tokenjam` always runs the release it shipped with. If the pinned
|
|
35
|
+
* spec can't be resolved yet (this wrapper published slightly ahead of PyPI
|
|
36
|
+
* propagation), we fall back to the unpinned form rather than fail outright.
|
|
37
|
+
*
|
|
38
|
+
* Staleness note: pinning only fixes what THIS wrapper runs. A bare `tj`
|
|
39
|
+
* invoked directly (no `npx`) still runs whatever was separately installed
|
|
40
|
+
* via `uv tool install` / `pipx install` / `pip install` / Homebrew, which can
|
|
41
|
+
* sit on an old version indefinitely. See `warnIfShadowedByStaleInstall`
|
|
42
|
+
* below: detect-and-tell only, never mutates, never auto-upgrades.
|
|
33
43
|
*/
|
|
34
44
|
"use strict";
|
|
35
45
|
|
|
36
46
|
const { spawnSync } = require("child_process");
|
|
37
|
-
const fs = require("fs");
|
|
38
|
-
const os = require("os");
|
|
39
47
|
const path = require("path");
|
|
40
48
|
|
|
41
49
|
// PyPI package name vs. command name differ (`tokenjam` ships the `tj` script),
|
|
@@ -49,87 +57,207 @@ function has(bin) {
|
|
|
49
57
|
return probe.status === 0 || probe.status === 1; // 1 = exists but no --version
|
|
50
58
|
}
|
|
51
59
|
|
|
52
|
-
|
|
60
|
+
// This wrapper's own version. `publish-npm.yml`'s wrapper-publish job runs
|
|
61
|
+
// `npm version ${GITHUB_REF_NAME#v}` against npm-wrapper/package.json before
|
|
62
|
+
// `npm publish`, so whatever version this file ships inside always matches
|
|
63
|
+
// the tokenjam release it was cut alongside — safe to read at runtime as the
|
|
64
|
+
// version to pin the Python side to.
|
|
65
|
+
function ownVersion() {
|
|
66
|
+
try {
|
|
67
|
+
return require(path.join(__dirname, "..", "package.json")).version;
|
|
68
|
+
} catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function runners(version) {
|
|
74
|
+
const pinnedSpec = version ? `${PACKAGE}==${version}` : null;
|
|
53
75
|
return [
|
|
54
|
-
{
|
|
55
|
-
|
|
56
|
-
|
|
76
|
+
{
|
|
77
|
+
bin: "uvx",
|
|
78
|
+
pinnedPrefix: pinnedSpec ? ["--from", pinnedSpec, COMMAND] : null,
|
|
79
|
+
prefix: ["--from", PACKAGE, COMMAND],
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
bin: "pipx",
|
|
83
|
+
pinnedPrefix: pinnedSpec ? ["run", "--spec", pinnedSpec, COMMAND] : null,
|
|
84
|
+
prefix: ["run", "--spec", PACKAGE, COMMAND],
|
|
85
|
+
},
|
|
86
|
+
{ bin: COMMAND, pinnedPrefix: null, prefix: [] }, // already installed on PATH, nothing to pin
|
|
57
87
|
];
|
|
58
88
|
}
|
|
59
89
|
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
function
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
xdgCacheHome && xdgCacheHome.trim()
|
|
74
|
-
? xdgCacheHome
|
|
75
|
-
: path.join(os.homedir(), ".cache");
|
|
76
|
-
return path.join(base, "tokenjam-npx");
|
|
90
|
+
// Cheap, side-effect-free resolution probe: does `<bin> <args> --version`
|
|
91
|
+
// succeed? Used to decide, before the real invocation, whether the pinned
|
|
92
|
+
// package spec actually resolves on this runner — falls back to the
|
|
93
|
+
// unpinned prefix when it doesn't (wrapper published ahead of PyPI
|
|
94
|
+
// propagation). Unlike `has()`, this requires an exact status 0: `uv`
|
|
95
|
+
// exits 1 both for "resolution failed" AND for "binary ran fine but
|
|
96
|
+
// doesn't understand --version", so treating 1 as success here would
|
|
97
|
+
// silently paper over real resolution failures (verified: `uvx --from
|
|
98
|
+
// tokenjam==<bogus> tj --version` also exits 1, indistinguishable from the
|
|
99
|
+
// unsupported-flag case `has()` is built around).
|
|
100
|
+
function resolves(bin, args) {
|
|
101
|
+
const probe = spawnSync(bin, [...args, "--version"], { stdio: "ignore" });
|
|
102
|
+
return probe.status === 0;
|
|
77
103
|
}
|
|
78
104
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
105
|
+
// --- stale shadowing install detection ------------------------------------
|
|
106
|
+
//
|
|
107
|
+
// Detect-and-tell only, per design: this never mutates anything and never
|
|
108
|
+
// upgrades on the user's behalf, in interactive or non-interactive/CI
|
|
109
|
+
// contexts alike. It's a best-effort nudge — any detection failure (missing
|
|
110
|
+
// binary, unexpected output, timeout) is swallowed and skipped silently; it
|
|
111
|
+
// must never break or slow down the primary command above by much.
|
|
82
112
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
function shouldRefresh() {
|
|
113
|
+
const DETECT_TIMEOUT_MS = 2000;
|
|
114
|
+
|
|
115
|
+
function safeSpawn(bin, args) {
|
|
87
116
|
try {
|
|
88
|
-
|
|
89
|
-
|
|
117
|
+
return spawnSync(bin, args, {
|
|
118
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
119
|
+
timeout: DETECT_TIMEOUT_MS,
|
|
120
|
+
encoding: "utf8",
|
|
121
|
+
});
|
|
90
122
|
} catch {
|
|
91
|
-
return
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function versionParts(v) {
|
|
128
|
+
return String(v)
|
|
129
|
+
.trim()
|
|
130
|
+
.split(".")
|
|
131
|
+
.map((n) => parseInt(n, 10) || 0);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function isOlder(a, b) {
|
|
135
|
+
const pa = versionParts(a);
|
|
136
|
+
const pb = versionParts(b);
|
|
137
|
+
const len = Math.max(pa.length, pb.length);
|
|
138
|
+
for (let i = 0; i < len; i++) {
|
|
139
|
+
const x = pa[i] || 0;
|
|
140
|
+
const y = pb[i] || 0;
|
|
141
|
+
if (x !== y) return x < y;
|
|
92
142
|
}
|
|
143
|
+
return false;
|
|
93
144
|
}
|
|
94
145
|
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
146
|
+
// Each detector below is skipped up front via `has()` when its own binary
|
|
147
|
+
// isn't even on PATH, so a machine without e.g. Homebrew never pays for a
|
|
148
|
+
// `brew list` spawn.
|
|
149
|
+
|
|
150
|
+
function detectUvTool() {
|
|
151
|
+
if (!has("uv")) return null;
|
|
152
|
+
const result = safeSpawn("uv", ["tool", "list"]);
|
|
153
|
+
if (!result || result.status !== 0 || !result.stdout) return null;
|
|
154
|
+
const match = result.stdout.match(/^tokenjam\s+v?(\S+)/m);
|
|
155
|
+
if (!match) return null;
|
|
156
|
+
return {
|
|
157
|
+
method: "uv tool",
|
|
158
|
+
version: match[1],
|
|
159
|
+
upgradeCmd: "uv tool upgrade tokenjam",
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function detectPipx() {
|
|
164
|
+
if (!has("pipx")) return null;
|
|
165
|
+
const result = safeSpawn("pipx", ["list", "--json"]);
|
|
166
|
+
if (!result || result.status !== 0 || !result.stdout) return null;
|
|
106
167
|
try {
|
|
107
|
-
|
|
108
|
-
|
|
168
|
+
const data = JSON.parse(result.stdout);
|
|
169
|
+
const venv = data.venvs && data.venvs[PACKAGE];
|
|
170
|
+
const version =
|
|
171
|
+
venv &&
|
|
172
|
+
venv.metadata &&
|
|
173
|
+
venv.metadata.main_package &&
|
|
174
|
+
venv.metadata.main_package.package_version;
|
|
175
|
+
if (!version) return null;
|
|
176
|
+
return { method: "pipx", version, upgradeCmd: "pipx upgrade tokenjam" };
|
|
109
177
|
} catch {
|
|
110
|
-
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function detectPip() {
|
|
183
|
+
for (const pipBin of ["pip3", "pip"]) {
|
|
184
|
+
if (!has(pipBin)) continue;
|
|
185
|
+
const result = safeSpawn(pipBin, ["show", PACKAGE]);
|
|
186
|
+
if (!result || result.status !== 0 || !result.stdout) continue;
|
|
187
|
+
const match = result.stdout.match(/^Version:\s*(\S+)/m);
|
|
188
|
+
if (!match) continue;
|
|
189
|
+
// Covers both a plain `pip install` and `pip install --user` — pip
|
|
190
|
+
// doesn't distinguish the two in `pip show` output, and either way the
|
|
191
|
+
// fix command is the same.
|
|
192
|
+
return {
|
|
193
|
+
method: "pip",
|
|
194
|
+
version: match[1],
|
|
195
|
+
upgradeCmd: `${pipBin} install --upgrade ${PACKAGE}`,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function detectHomebrew() {
|
|
202
|
+
if (!has("brew")) return null;
|
|
203
|
+
const result = safeSpawn("brew", ["list", "--versions", PACKAGE]);
|
|
204
|
+
if (!result || result.status !== 0 || !result.stdout) return null;
|
|
205
|
+
const match = result.stdout.trim().match(/^tokenjam\s+(\S+)/);
|
|
206
|
+
if (!match) return null;
|
|
207
|
+
return {
|
|
208
|
+
method: "Homebrew",
|
|
209
|
+
version: match[1],
|
|
210
|
+
upgradeCmd: "brew upgrade tokenjam",
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function warnIfShadowedByStaleInstall(wrapperVersion) {
|
|
215
|
+
if (!wrapperVersion) return;
|
|
216
|
+
const detectors = [detectUvTool, detectPipx, detectPip, detectHomebrew];
|
|
217
|
+
for (const detect of detectors) {
|
|
218
|
+
let found = null;
|
|
219
|
+
try {
|
|
220
|
+
found = detect();
|
|
221
|
+
} catch {
|
|
222
|
+
found = null;
|
|
223
|
+
}
|
|
224
|
+
if (!found || !isOlder(found.version, wrapperVersion)) continue;
|
|
225
|
+
process.stderr.write(
|
|
226
|
+
"\n" +
|
|
227
|
+
`Note: a ${found.method} install of tokenjam is at v${found.version}, older than v${wrapperVersion} run here.\n` +
|
|
228
|
+
`Upgrade it with: ${found.upgradeCmd}\n`
|
|
229
|
+
);
|
|
230
|
+
return; // one line is enough — first stale install found wins
|
|
111
231
|
}
|
|
112
232
|
}
|
|
113
233
|
|
|
114
234
|
function main() {
|
|
115
|
-
// Bare `npx tokenjam` IS the zero-install first run —
|
|
116
|
-
//
|
|
117
|
-
//
|
|
118
|
-
//
|
|
119
|
-
//
|
|
235
|
+
// Bare `npx tokenjam` IS the zero-install first run — the quota report the
|
|
236
|
+
// docs promise. The branded home screen that bare LOCAL `tj` prints assumes
|
|
237
|
+
// an installed CLI and would dead-end an npx user ("You're set up",
|
|
238
|
+
// suggesting commands they don't have). Any explicit args pass through
|
|
239
|
+
// untouched; a bare invocation stays bare (no synthetic subcommand — there
|
|
240
|
+
// is no public/typeable command for this) and instead sets an env var that
|
|
241
|
+
// the Python CLI's own no-subcommand branch reads to pick the report over
|
|
242
|
+
// the home screen.
|
|
120
243
|
const argv = process.argv.slice(2);
|
|
121
|
-
const passthrough = argv
|
|
244
|
+
const passthrough = argv;
|
|
245
|
+
const childEnv = argv.length
|
|
246
|
+
? process.env
|
|
247
|
+
: { ...process.env, TJ_NPX_ZERO_INSTALL_REPORT: "1" };
|
|
248
|
+
|
|
249
|
+
const version = ownVersion();
|
|
122
250
|
|
|
123
|
-
for (const { bin, prefix } of runners()) {
|
|
251
|
+
for (const { bin, pinnedPrefix, prefix } of runners(version)) {
|
|
124
252
|
if (!has(bin)) continue;
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
const args = doRefresh ? ["--refresh", ...prefix] : prefix;
|
|
253
|
+
const args =
|
|
254
|
+
pinnedPrefix && resolves(bin, pinnedPrefix) ? pinnedPrefix : prefix;
|
|
128
255
|
const result = spawnSync(bin, [...args, ...passthrough], {
|
|
129
256
|
stdio: "inherit",
|
|
257
|
+
env: childEnv,
|
|
130
258
|
});
|
|
131
259
|
if (result.error) continue; // try the next runner on spawn failure
|
|
132
|
-
|
|
260
|
+
warnIfShadowedByStaleInstall(version);
|
|
133
261
|
process.exit(result.status === null ? 1 : result.status);
|
|
134
262
|
}
|
|
135
263
|
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tokenjam",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Zero-install launcher for TokenJam (tj): npx tokenjam runs the Python CLI via uvx/pipx and
|
|
3
|
+
"version": "0.6.1",
|
|
4
|
+
"description": "Zero-install launcher for TokenJam (tj): npx tokenjam runs the Python CLI via uvx/pipx and reports the recurring mistakes your AI agent keeps repeating, no setup required.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude-code",
|
|
7
7
|
"ccusage",
|
|
8
8
|
"tokens",
|
|
9
|
-
"cost",
|
|
9
|
+
"cost-optimization",
|
|
10
10
|
"llm",
|
|
11
11
|
"agents",
|
|
12
12
|
"observability",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"anthropic",
|
|
15
15
|
"claude",
|
|
16
16
|
"token-usage",
|
|
17
|
-
"
|
|
17
|
+
"agent-behavior",
|
|
18
18
|
"cli",
|
|
19
19
|
"statusline",
|
|
20
20
|
"opentelemetry",
|