savemytokens 0.2.2 → 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
@@ -191,9 +191,9 @@ bar the built-in themes have to clear.
191
191
  beyond those two facts would be a guess dressed up as a number.
192
192
  - **It cannot see usage it did not measure.** Another machine, claude.ai, or work done before you
193
193
  installed it shows up as unattributed window, labelled as such.
194
- - **An idle window still holds its share.** A session with nothing running keeps its slice rather
195
- than lending it out, so ten open terminals divide the window ten ways. Treating a target as a
196
- 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.
197
197
  - **It makes no network call.** No account, no telemetry, no daemon. `savemytokens privacy` prints
198
198
  every file it reads and writes.
199
199
 
@@ -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.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,7 +40,7 @@
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 && rsvg-convert -w 1280 -h 640 assets/social.svg -o assets/social.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",
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
44
  "prepublishOnly": "npm run build && npm test",
45
45
  "postpublish": "node scripts/release.mjs",
46
46
  "release": "node scripts/release.mjs",