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 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="730">
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
- | `npx savemytokens` | the control centre |
89
- | `npx savemytokens status` | one plain-text snapshot · `--json` for a script |
90
- | `npx savemytokens share buydiff 40` | pin a project's allocation |
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
- - **An idle window still holds its share.** A session with nothing running keeps its slice rather
202
- than lending it out, so ten open terminals divide the window ten ways. Treating a target as a
203
- weight among sessions actually consuming is the next fix.
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
 
@@ -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");
@@ -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 publishes your 5h and 7d usage, and it is what proves a session is still open. Run: npx savemytokens install", context));
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;
@@ -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
- const state = entry.state;
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 (typeof entry.share === "number" && entry.share > keep) released += entry.share - keep;
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((entry) => typeof entry.share === "number" && entry.share >= 0);
716
- const free = eligible.filter((entry) => !(typeof entry.share === "number" && entry.share >= 0));
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
- if (!(target > MIN_TARGET)) return { value: consumedShare > 0 ? MAX_PRESSURE : 0, basis: "share" };
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) * (consumedShare / target)), basis: "budget" };
960
+ return { value: Math.min(MAX_PRESSURE, (quotaUsedPercent / 100) * ratio), basis: "budget" };
951
961
  }
952
- return { value: Math.min(MAX_PRESSURE, consumedShare / target), basis: "share" };
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.1",
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",