savemytokens 0.2.0
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/LICENSE +21 -0
- package/README.md +164 -0
- package/dist/adapters/claude-code/index.js +130 -0
- package/dist/adapters/claude-code/merge.js +90 -0
- package/dist/adapters/claude-code/parse.js +642 -0
- package/dist/adapters/claude-code/provider.js +105 -0
- package/dist/adapters/codex/index.js +74 -0
- package/dist/adapters/codex/parse.js +389 -0
- package/dist/adapters/codex/provider.js +171 -0
- package/dist/adapters/index.js +10 -0
- package/dist/adapters/pending.js +29 -0
- package/dist/adapters/types.js +1 -0
- package/dist/analyze/aggregate.js +180 -0
- package/dist/analyze/combine.js +11 -0
- package/dist/analyze/detectors.js +244 -0
- package/dist/analyze/index.js +29 -0
- package/dist/analyze/score.js +20 -0
- package/dist/cli-options.js +149 -0
- package/dist/cli.js +141 -0
- package/dist/collect.js +62 -0
- package/dist/commands/audit.js +74 -0
- package/dist/commands/control.js +654 -0
- package/dist/commands/hud.js +71 -0
- package/dist/commands/install.js +369 -0
- package/dist/commands/policy.js +93 -0
- package/dist/commands/privacy.js +28 -0
- package/dist/commands/set.js +83 -0
- package/dist/commands/theme.js +136 -0
- package/dist/commands/watch.js +135 -0
- package/dist/core/cost.js +24 -0
- package/dist/core/hash.js +0 -0
- package/dist/core/pricing.js +63 -0
- package/dist/core/resource.js +1 -0
- package/dist/core/tokens.js +32 -0
- package/dist/core/types.js +1 -0
- package/dist/hooks/nudge.js +111 -0
- package/dist/hooks/rules.js +14 -0
- package/dist/privacy/payload.js +22 -0
- package/dist/report/graph.js +162 -0
- package/dist/report/graphs.js +61 -0
- package/dist/report/render.js +183 -0
- package/dist/report/schedule.js +143 -0
- package/dist/report/settings.js +237 -0
- package/dist/report/views.js +418 -0
- package/dist/runtime/hook.mjs +234 -0
- package/dist/runtime/kernel.mjs +1472 -0
- package/dist/runtime/statusline.mjs +243 -0
- package/dist/scheduler/keys.js +112 -0
- package/dist/scheduler/plan.js +287 -0
- package/dist/storage/cache.js +38 -0
- package/dist/storage/paths.js +29 -0
- package/dist/storage/store.js +48 -0
- package/dist/util/ansi.js +35 -0
- package/dist/util/fmt.js +76 -0
- package/package.json +51 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
export const HOME = process.env.SAVEMYTOKENS_HOME || path.join(os.homedir(), ".savemytokens");
|
|
5
|
+
export const CACHE_DIR = path.join(HOME, "cache");
|
|
6
|
+
export const RUNS_FILE = path.join(HOME, "runs.json");
|
|
7
|
+
export const LAST_AUDIT_FILE = path.join(HOME, "last-audit.json");
|
|
8
|
+
export const CONFIG_FILE = path.join(HOME, "config.json");
|
|
9
|
+
export function ensureHome() {
|
|
10
|
+
fs.mkdirSync(CACHE_DIR, { recursive: true });
|
|
11
|
+
}
|
|
12
|
+
export function displayHome() {
|
|
13
|
+
const home = os.homedir();
|
|
14
|
+
return HOME.startsWith(home) ? "~" + HOME.slice(home.length) : HOME;
|
|
15
|
+
}
|
|
16
|
+
export function readJson(file, fallback) {
|
|
17
|
+
try {
|
|
18
|
+
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return fallback;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export function writeJson(file, value) {
|
|
25
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
26
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
27
|
+
fs.writeFileSync(tmp, JSON.stringify(value));
|
|
28
|
+
fs.renameSync(tmp, file);
|
|
29
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { LAST_AUDIT_FILE, RUNS_FILE, ensureHome, readJson, writeJson } from "./paths.js";
|
|
2
|
+
const MAX_RUNS = 250;
|
|
3
|
+
export function loadRuns() {
|
|
4
|
+
const runs = readJson(RUNS_FILE, []);
|
|
5
|
+
return Array.isArray(runs) ? runs : [];
|
|
6
|
+
}
|
|
7
|
+
export function scopeKey(audit) {
|
|
8
|
+
return `${audit.scope.days}d:${audit.scope.project ?? "all"}`;
|
|
9
|
+
}
|
|
10
|
+
export function previousRun(audit, runs) {
|
|
11
|
+
const key = scopeKey(audit);
|
|
12
|
+
for (let i = runs.length - 1; i >= 0; i--) {
|
|
13
|
+
const run = runs[i];
|
|
14
|
+
if (!run)
|
|
15
|
+
continue;
|
|
16
|
+
if (`${run.scope.days}d:${run.scope.project ?? "all"}` !== key)
|
|
17
|
+
continue;
|
|
18
|
+
if (run.ranAt >= audit.ranAt)
|
|
19
|
+
continue;
|
|
20
|
+
return run;
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
export function toRunRecord(audit) {
|
|
25
|
+
return {
|
|
26
|
+
ranAt: audit.ranAt,
|
|
27
|
+
score: audit.score,
|
|
28
|
+
wasteRatio: audit.wasteRatio,
|
|
29
|
+
upliftRatio: audit.upliftRatio,
|
|
30
|
+
scope: audit.scope,
|
|
31
|
+
totals: audit.totals,
|
|
32
|
+
findings: audit.findings.map((f) => ({
|
|
33
|
+
id: f.id,
|
|
34
|
+
title: f.title,
|
|
35
|
+
wasteRatio: f.wasteRatio,
|
|
36
|
+
confidence: f.confidence,
|
|
37
|
+
})),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
export function saveRun(audit) {
|
|
41
|
+
ensureHome();
|
|
42
|
+
const runs = loadRuns();
|
|
43
|
+
runs.push(toRunRecord(audit));
|
|
44
|
+
const trimmed = runs.slice(-MAX_RUNS);
|
|
45
|
+
writeJson(RUNS_FILE, trimmed);
|
|
46
|
+
writeJson(LAST_AUDIT_FILE, audit);
|
|
47
|
+
return trimmed;
|
|
48
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const ESC = "\u001b[";
|
|
2
|
+
const RESET = `${ESC}0m`;
|
|
3
|
+
const enabled = process.env.NO_COLOR === undefined && process.env.TERM !== "dumb" && Boolean(process.stdout.isTTY);
|
|
4
|
+
function wrap(code) {
|
|
5
|
+
return (text) => (enabled ? `${ESC}${code}m${text}${RESET}` : text);
|
|
6
|
+
}
|
|
7
|
+
export const bold = wrap("1");
|
|
8
|
+
export const dim = wrap("2");
|
|
9
|
+
export const green = wrap("32");
|
|
10
|
+
export const yellow = wrap("33");
|
|
11
|
+
export const red = wrap("31");
|
|
12
|
+
export const colorEnabled = enabled;
|
|
13
|
+
const ANSI_PATTERN = new RegExp("\\u001b\\[[0-9;]*m", "g");
|
|
14
|
+
export function stripAnsi(text) {
|
|
15
|
+
return text.replace(ANSI_PATTERN, "");
|
|
16
|
+
}
|
|
17
|
+
export function visibleWidth(text) {
|
|
18
|
+
return stripAnsi(text).length;
|
|
19
|
+
}
|
|
20
|
+
export function padEndVisible(text, width) {
|
|
21
|
+
const pad = width - visibleWidth(text);
|
|
22
|
+
return pad > 0 ? text + " ".repeat(pad) : text;
|
|
23
|
+
}
|
|
24
|
+
export function padStartVisible(text, width) {
|
|
25
|
+
const pad = width - visibleWidth(text);
|
|
26
|
+
return pad > 0 ? " ".repeat(pad) + text : text;
|
|
27
|
+
}
|
|
28
|
+
export function clip(text, max) {
|
|
29
|
+
if (max <= 1)
|
|
30
|
+
return "";
|
|
31
|
+
const visible = visibleWidth(text);
|
|
32
|
+
if (visible <= max)
|
|
33
|
+
return text;
|
|
34
|
+
return `${text.slice(0, Math.max(0, text.length - (visible - max) - 1))}…`;
|
|
35
|
+
}
|
package/dist/util/fmt.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
export function compactNumber(value) {
|
|
2
|
+
const n = Math.round(value);
|
|
3
|
+
if (n < 1_000)
|
|
4
|
+
return String(n);
|
|
5
|
+
if (n < 999_500)
|
|
6
|
+
return `${(n / 1_000).toFixed(n < 10_000 ? 1 : 0)}k`;
|
|
7
|
+
if (n < 999_500_000)
|
|
8
|
+
return `${(n / 1_000_000).toFixed(n < 10_000_000 ? 1 : 0)}M`;
|
|
9
|
+
return `${(n / 1_000_000_000).toFixed(1)}B`;
|
|
10
|
+
}
|
|
11
|
+
export function bytes(chars) {
|
|
12
|
+
if (chars < 1_024)
|
|
13
|
+
return `${chars} B`;
|
|
14
|
+
if (chars < 1_024 * 1_024)
|
|
15
|
+
return `${(chars / 1_024).toFixed(0)} KB`;
|
|
16
|
+
return `${(chars / (1_024 * 1_024)).toFixed(1)} MB`;
|
|
17
|
+
}
|
|
18
|
+
export function money(value) {
|
|
19
|
+
if (value >= 1000)
|
|
20
|
+
return `$${Math.round(value).toLocaleString("en-US")}`;
|
|
21
|
+
if (value >= 10)
|
|
22
|
+
return `$${Math.round(value)}`;
|
|
23
|
+
if (value >= 1)
|
|
24
|
+
return `$${value.toFixed(1)}`;
|
|
25
|
+
return `$${value.toFixed(2)}`;
|
|
26
|
+
}
|
|
27
|
+
export function percent(ratio, digits = 0) {
|
|
28
|
+
return `${(ratio * 100).toFixed(digits)}%`;
|
|
29
|
+
}
|
|
30
|
+
const SPARK = "▁▂▃▄▅▆▇█";
|
|
31
|
+
export function bar(value, max, width = 8) {
|
|
32
|
+
if (max <= 0 || value <= 0)
|
|
33
|
+
return "";
|
|
34
|
+
const filled = Math.round((value / max) * width);
|
|
35
|
+
return "▇".repeat(Math.max(1, Math.min(width, filled)));
|
|
36
|
+
}
|
|
37
|
+
export function sparkline(values) {
|
|
38
|
+
if (values.length < 3)
|
|
39
|
+
return "";
|
|
40
|
+
const min = Math.min(...values);
|
|
41
|
+
const max = Math.max(...values);
|
|
42
|
+
if (max === min)
|
|
43
|
+
return "▄".repeat(values.length);
|
|
44
|
+
const span = max - min;
|
|
45
|
+
return values
|
|
46
|
+
.map((value) => SPARK[Math.min(SPARK.length - 1, Math.floor(((value - min) / span) * (SPARK.length - 1)))] ?? " ")
|
|
47
|
+
.join("");
|
|
48
|
+
}
|
|
49
|
+
export function shortPath(value, max = 44) {
|
|
50
|
+
if (value.length <= max)
|
|
51
|
+
return value;
|
|
52
|
+
const parts = value.split("/").filter(Boolean);
|
|
53
|
+
const tail = parts.slice(-2).join("/");
|
|
54
|
+
return tail.length <= max ? `…/${tail}` : `…/${parts[parts.length - 1] ?? value}`;
|
|
55
|
+
}
|
|
56
|
+
export function plural(count, one, many = `${one}s`) {
|
|
57
|
+
return count === 1 ? one : many;
|
|
58
|
+
}
|
|
59
|
+
export function ago(ts, now = Date.now()) {
|
|
60
|
+
const diff = Math.max(0, now - ts);
|
|
61
|
+
const minutes = Math.round(diff / 60_000);
|
|
62
|
+
if (minutes < 1)
|
|
63
|
+
return "just now";
|
|
64
|
+
if (minutes < 60)
|
|
65
|
+
return `${minutes}m ago`;
|
|
66
|
+
const hours = Math.round(minutes / 60);
|
|
67
|
+
if (hours < 24)
|
|
68
|
+
return `${hours}h ago`;
|
|
69
|
+
const days = Math.round(hours / 24);
|
|
70
|
+
return `${days}d ago`;
|
|
71
|
+
}
|
|
72
|
+
export function shortDate(ts) {
|
|
73
|
+
const d = new Date(ts);
|
|
74
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
75
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
|
76
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "savemytokens",
|
|
3
|
+
"version": "0.2.0",
|
|
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
|
+
"keywords": [
|
|
6
|
+
"claude",
|
|
7
|
+
"claude-code",
|
|
8
|
+
"statusline",
|
|
9
|
+
"rate-limit",
|
|
10
|
+
"quota",
|
|
11
|
+
"scheduler",
|
|
12
|
+
"tokens",
|
|
13
|
+
"cost",
|
|
14
|
+
"tui",
|
|
15
|
+
"cli"
|
|
16
|
+
],
|
|
17
|
+
"homepage": "https://savemytokens.com",
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/offbeatport/savemytokens.git"
|
|
21
|
+
},
|
|
22
|
+
"bugs": {
|
|
23
|
+
"url": "https://github.com/offbeatport/savemytokens/issues"
|
|
24
|
+
},
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"author": "Offbeatport",
|
|
27
|
+
"type": "module",
|
|
28
|
+
"bin": {
|
|
29
|
+
"savemytokens": "dist/cli.js"
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"dist",
|
|
33
|
+
"README.md",
|
|
34
|
+
"LICENSE"
|
|
35
|
+
],
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=18.17"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "tsc -p tsconfig.json && node scripts/copy-runtime.mjs",
|
|
41
|
+
"dev": "npm run build && node dist/cli.js",
|
|
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"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@types/node": "^22.10.2",
|
|
48
|
+
"typescript": "^5.7.2"
|
|
49
|
+
},
|
|
50
|
+
"packageManager": "pnpm@10.30.3+sha256.ff0a72140f6a6d66c0b284f6c9560aff605518e28c29aeac25fb262b74331588"
|
|
51
|
+
}
|