savemytokens 0.2.1 → 0.2.3
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 +11 -18
- package/dist/commands/install.js +10 -0
- package/dist/report/views.js +1 -1
- package/dist/runtime/kernel.mjs +19 -9
- package/package.json +6 -3
package/README.md
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
|
|
7
7
|
### Decide which projects get your Claude allowance.
|
|
8
8
|
|
|
9
|
+
<sub>A capacity scheduler for Claude Code · Node 18.17+ · nothing leaves your machine</sub>
|
|
10
|
+
|
|
9
11
|
<a href="https://www.npmjs.com/package/savemytokens"><img src="https://img.shields.io/npm/v/savemytokens?style=flat-square&color=1a7f37&label=npm" alt="npm version"></a>
|
|
10
12
|
<img src="https://img.shields.io/badge/dependencies-0-1a7f37?style=flat-square" alt="zero dependencies">
|
|
11
13
|
<img src="https://img.shields.io/badge/node-%E2%89%A5%2018.17-2b7489?style=flat-square" alt="node 18.17+">
|
|
@@ -71,7 +73,7 @@ burningdemand, because it is the only project on the high tier.
|
|
|
71
73
|
|
|
72
74
|
<div align="center">
|
|
73
75
|
|
|
74
|
-
<img src="https://raw.githubusercontent.com/offbeatport/savemytokens/main/assets/example.svg" alt="Seven projects in the ACTIVE table. Three are pinned, in the order they were pinned: burningdemand at 20% and 71% through it on HIGH, autonomykernel at 18% and 44% through, coldverdict at 12% and 93% through on LOW. Below them the unpinned ones share what is left." width="
|
|
76
|
+
<img src="https://raw.githubusercontent.com/offbeatport/savemytokens/main/assets/example.svg" alt="Seven projects in the ACTIVE table. Three are pinned, in the order they were pinned: burningdemand at 20% and 71% through it on HIGH, autonomykernel at 18% and 44% through, coldverdict at 12% and 93% through on LOW. Below them the unpinned ones share what is left." width="955">
|
|
75
77
|
|
|
76
78
|
</div>
|
|
77
79
|
|
|
@@ -83,20 +85,11 @@ them. Nothing here required you to guess how many tokens anything costs.
|
|
|
83
85
|
|
|
84
86
|
## Commands
|
|
85
87
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
| `npx savemytokens priority buydiff high` | who gets spare capacity first |
|
|
92
|
-
| `npx savemytokens release buydiff` | hand its unused share back |
|
|
93
|
-
| `npx savemytokens policy strict` | wind down earlier when it gets tight |
|
|
94
|
-
| `npx savemytokens hud` | pick a status line, previewed on your numbers |
|
|
95
|
-
| `npx savemytokens theme` | eighteen themes, or write your own |
|
|
96
|
-
| `npx savemytokens defer` | work a session pushed to next time |
|
|
97
|
-
| `npx savemytokens audit` | what your last 7 days wasted |
|
|
98
|
-
| `npx savemytokens privacy` | every file it reads and writes |
|
|
99
|
-
| `npx savemytokens uninstall` | remove every trace · `--purge` drops the data too |
|
|
88
|
+
<div align="center">
|
|
89
|
+
|
|
90
|
+
<img src="https://raw.githubusercontent.com/offbeatport/savemytokens/main/assets/commands.svg" alt="The command list: npx savemytokens for the control centre, status, share, priority, release, policy, hud, theme, defer, audit, privacy and uninstall." width="955">
|
|
91
|
+
|
|
92
|
+
</div>
|
|
100
93
|
|
|
101
94
|
`--codex` points any of these at Codex instead, which publishes its own rate limits and needs no
|
|
102
95
|
hooks. It is metered and shown, but nothing can be injected into it, so there is no advice.
|
|
@@ -198,9 +191,9 @@ bar the built-in themes have to clear.
|
|
|
198
191
|
beyond those two facts would be a guess dressed up as a number.
|
|
199
192
|
- **It cannot see usage it did not measure.** Another machine, claude.ai, or work done before you
|
|
200
193
|
installed it shows up as unattributed window, labelled as such.
|
|
201
|
-
- **
|
|
202
|
-
|
|
203
|
-
|
|
194
|
+
- **The split moves under you.** A window that has taken no turn in ten minutes lends its share to
|
|
195
|
+
the rest, and takes it back the moment you type in it again. What is on screen is the current
|
|
196
|
+
split, not a contract: a project's allocation can change without you touching anything.
|
|
204
197
|
- **It makes no network call.** No account, no telemetry, no daemon. `savemytokens privacy` prints
|
|
205
198
|
every file it reads and writes.
|
|
206
199
|
|
package/dist/commands/install.js
CHANGED
|
@@ -105,6 +105,9 @@ function writeRules() {
|
|
|
105
105
|
export function rulesInstalled() {
|
|
106
106
|
return readMemory().includes(RULES_START);
|
|
107
107
|
}
|
|
108
|
+
export function claudeCodeFound() {
|
|
109
|
+
return fs.existsSync(CLAUDE_HOME) || fs.existsSync(SETTINGS);
|
|
110
|
+
}
|
|
108
111
|
export function hookInstalled() {
|
|
109
112
|
return fs.existsSync(hookPath("hook.mjs"));
|
|
110
113
|
}
|
|
@@ -140,6 +143,7 @@ export function runInstall(options) {
|
|
|
140
143
|
process.exitCode = 1;
|
|
141
144
|
return;
|
|
142
145
|
}
|
|
146
|
+
const foundClaudeCode = claudeCodeFound();
|
|
143
147
|
const settings = parsed;
|
|
144
148
|
const existingStatusLine = settings.statusLine?.command;
|
|
145
149
|
const ours = ourCommand(existingStatusLine);
|
|
@@ -223,6 +227,12 @@ export function runInstall(options) {
|
|
|
223
227
|
writeSettings(settings);
|
|
224
228
|
out.push(`${green("Installed.")} It takes effect in sessions you start from now on.`);
|
|
225
229
|
out.push(dim(" Open the control centre with: npx savemytokens"));
|
|
230
|
+
if (!foundClaudeCode) {
|
|
231
|
+
out.push("");
|
|
232
|
+
out.push(yellow(" Claude Code was not found."));
|
|
233
|
+
out.push(dim(` Nothing was seen at ${CLAUDE_HOME}, so this machine may not have it installed.`));
|
|
234
|
+
out.push(dim(" The hooks are in place and will start working the first time you run it."));
|
|
235
|
+
}
|
|
226
236
|
out.push("");
|
|
227
237
|
if (!options.quiet)
|
|
228
238
|
process.stdout.write(out.join("\n") + "\n");
|
package/dist/report/views.js
CHANGED
|
@@ -419,7 +419,7 @@ export function helpOverlay(control, context) {
|
|
|
419
419
|
out.push(...helpSection("the status line", context));
|
|
420
420
|
out.push(...helpProse(control.installed
|
|
421
421
|
? "Installed. It is the only place your 5h and 7d usage is published, and it is what proves a session is still open, so without it nothing here is live. Change its shape with P."
|
|
422
|
-
: "Not installed, so nothing here is live. It is the only place your 5h and 7d usage is
|
|
422
|
+
: "Not installed, so nothing here is live. It is the only place your 5h and 7d usage is published, and it is what proves a session is still open. Run: npx savemytokens install", context));
|
|
423
423
|
out.push(...helpSection("the numbers", context));
|
|
424
424
|
out.push(...helpProse("5h and 7d are published figures, not ours. Share is measured from the tokens in your transcripts. Used of it is their number split by that share, so the split between rows is ours, not theirs.", context));
|
|
425
425
|
const drift = control.unattributed ?? 0;
|
package/dist/runtime/kernel.mjs
CHANGED
|
@@ -684,21 +684,30 @@ export function usageInWindow(record, from, to) {
|
|
|
684
684
|
return { ...total, tokens, weighted };
|
|
685
685
|
}
|
|
686
686
|
|
|
687
|
+
const isPinned = (entry) => typeof entry.share === "number" && entry.share >= 0;
|
|
688
|
+
const isRunning = (entry) => entry.state === "active" || entry.state === "needs-more";
|
|
689
|
+
const holdsAClaim = (entry) => isRunning(entry) || isPinned(entry) || (entry.consumed || 0) > 0;
|
|
690
|
+
|
|
687
691
|
export function allocate(entries) {
|
|
688
692
|
const targets = new Map();
|
|
689
693
|
const eligible = [];
|
|
690
694
|
let reserved = 0;
|
|
691
695
|
let released = 0;
|
|
692
696
|
|
|
697
|
+
const claiming = entries.filter(holdsAClaim);
|
|
698
|
+
const pinnedClaims = claiming.reduce((sum, entry) => (isPinned(entry) ? sum + entry.share : sum), 0);
|
|
699
|
+
const openClaims = claiming.filter((entry) => !isPinned(entry)).length;
|
|
700
|
+
const evenClaim = openClaims > 0 ? Math.max(0, 1 - Math.min(1, pinnedClaims)) / openClaims : 0;
|
|
701
|
+
|
|
693
702
|
for (const entry of entries) {
|
|
694
|
-
|
|
695
|
-
if (state === "active" || state === "needs-more") {
|
|
703
|
+
if (isRunning(entry)) {
|
|
696
704
|
eligible.push(entry);
|
|
697
705
|
continue;
|
|
698
706
|
}
|
|
699
707
|
const keep = Math.max(0, Math.min(1, entry.consumed || 0));
|
|
708
|
+
const nominal = isPinned(entry) ? entry.share : holdsAClaim(entry) ? evenClaim : 0;
|
|
700
709
|
reserved += keep;
|
|
701
|
-
if (
|
|
710
|
+
if (nominal > keep) released += nominal - keep;
|
|
702
711
|
targets.set(entry.id, { claimantId: entry.id, target: keep, pinned: false, pool: 0, released: true });
|
|
703
712
|
}
|
|
704
713
|
|
|
@@ -712,8 +721,8 @@ export function allocate(entries) {
|
|
|
712
721
|
let budget = Math.max(0, 1 - Math.min(1, reserved));
|
|
713
722
|
if (eligible.length === 0) return { targets, unusedPool: budget };
|
|
714
723
|
|
|
715
|
-
const pinned = eligible.filter(
|
|
716
|
-
const free = eligible.filter((entry) => !(
|
|
724
|
+
const pinned = eligible.filter(isPinned);
|
|
725
|
+
const free = eligible.filter((entry) => !isPinned(entry));
|
|
717
726
|
const pinnedSum = pinned.reduce((sum, entry) => sum + entry.share, 0);
|
|
718
727
|
const scale = pinnedSum > budget && pinnedSum > 0 ? budget / pinnedSum : 1;
|
|
719
728
|
|
|
@@ -941,15 +950,16 @@ export function viewFor(plan, id) {
|
|
|
941
950
|
return plan.claimants.find((view) => view.claimant.id === id) ?? null;
|
|
942
951
|
}
|
|
943
952
|
|
|
944
|
-
const MIN_TARGET = 0.001;
|
|
945
953
|
const MAX_PRESSURE = 9.99;
|
|
946
954
|
|
|
947
955
|
export function pressureFor(consumedShare, target, quotaUsedPercent) {
|
|
948
|
-
|
|
956
|
+
const spent = consumedShare > 0 ? consumedShare : 0;
|
|
957
|
+
if (!(target > 0)) return { value: spent > 0 ? MAX_PRESSURE : 0, basis: "share" };
|
|
958
|
+
const ratio = spent / target;
|
|
949
959
|
if (typeof quotaUsedPercent === "number" && quotaUsedPercent >= 0) {
|
|
950
|
-
return { value: Math.min(MAX_PRESSURE, (quotaUsedPercent / 100) *
|
|
960
|
+
return { value: Math.min(MAX_PRESSURE, (quotaUsedPercent / 100) * ratio), basis: "budget" };
|
|
951
961
|
}
|
|
952
|
-
return { value: Math.min(MAX_PRESSURE,
|
|
962
|
+
return { value: Math.min(MAX_PRESSURE, ratio), basis: "share" };
|
|
953
963
|
}
|
|
954
964
|
|
|
955
965
|
export const POLICIES = {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "savemytokens",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "Give every Claude Code session a target share of your Claude window, and keep Claude aware of it. Local, private, zero setup.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude",
|
|
@@ -40,8 +40,11 @@
|
|
|
40
40
|
"build": "tsc -p tsconfig.json && node scripts/copy-runtime.mjs",
|
|
41
41
|
"dev": "npm run build && node dist/cli.js",
|
|
42
42
|
"test": "npm run build && node --test test/*.test.js",
|
|
43
|
-
"screenshot": "npm run build && COLORTERM=truecolor node scripts/screenshot.mjs",
|
|
44
|
-
"prepublishOnly": "npm run build && npm test"
|
|
43
|
+
"screenshot": "npm run build && COLORTERM=truecolor node scripts/screenshot.mjs && rsvg-convert -w 1280 -h 640 assets/social.svg -o assets/social.png && cp assets/social.png web/og-image.png && cp assets/screenshot.svg assets/statusline.svg assets/f-numbers.svg assets/f-share.svg assets/f-agent.svg assets/f-resting.svg assets/f-defer.svg web/ && node scripts/stamp-assets.mjs",
|
|
44
|
+
"prepublishOnly": "npm run build && npm test",
|
|
45
|
+
"postpublish": "node scripts/release.mjs",
|
|
46
|
+
"release": "node scripts/release.mjs",
|
|
47
|
+
"drift": "node scripts/drift.mjs"
|
|
45
48
|
},
|
|
46
49
|
"devDependencies": {
|
|
47
50
|
"@types/node": "^22.10.2",
|