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,234 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
addDeferred,
|
|
5
|
+
adviceFor,
|
|
6
|
+
consumeSignal,
|
|
7
|
+
deferredAdvice,
|
|
8
|
+
loadConfig,
|
|
9
|
+
loadDeferred,
|
|
10
|
+
loadMeter,
|
|
11
|
+
openingAdvice,
|
|
12
|
+
policyFor,
|
|
13
|
+
sampleFiles,
|
|
14
|
+
schedule,
|
|
15
|
+
stageFor,
|
|
16
|
+
upsertClaimant,
|
|
17
|
+
viewFor,
|
|
18
|
+
writeJson,
|
|
19
|
+
readJson,
|
|
20
|
+
HOME,
|
|
21
|
+
} from "./kernel.mjs";
|
|
22
|
+
|
|
23
|
+
const ADAPTER = "claude-code";
|
|
24
|
+
const NUDGE_FILE = path.join(HOME, "nudges.json");
|
|
25
|
+
const NUDGE_THRESHOLD = 150000;
|
|
26
|
+
const NUDGE_COOLDOWN_MS = 30 * 60 * 1000;
|
|
27
|
+
const NUDGE_HORIZON_TURNS = 10;
|
|
28
|
+
const NUDGE_TAIL_BYTES = 262144;
|
|
29
|
+
const RATE_PER_TOKEN = 5 / 1000000;
|
|
30
|
+
const CACHE_READ_WEIGHT = 0.1;
|
|
31
|
+
const MAX_SUBAGENT_DEPTH = 4;
|
|
32
|
+
const ANAPHORIC =
|
|
33
|
+
/^\s*(ok|okay|now|also|and|then|next|again|yes|no|nope|yep|same|do the same|the other|these|those|them|it|that|this|continue|carry on|keep going|go on|more|another|fix (it|that|this)|try again|redo|revert|undo|hmm|wait|great|nice|thanks|perfect|good)\b/i;
|
|
34
|
+
|
|
35
|
+
function readInput() {
|
|
36
|
+
try {
|
|
37
|
+
const raw = fs.readFileSync(0, "utf8");
|
|
38
|
+
return raw ? JSON.parse(raw) : null;
|
|
39
|
+
} catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function nested(dir, depth, out) {
|
|
45
|
+
if (depth > MAX_SUBAGENT_DEPTH) return;
|
|
46
|
+
let entries;
|
|
47
|
+
try {
|
|
48
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
49
|
+
} catch {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
for (const entry of entries) {
|
|
53
|
+
const full = path.join(dir, entry.name);
|
|
54
|
+
if (entry.isDirectory()) nested(full, depth + 1, out);
|
|
55
|
+
else if (entry.name.endsWith(".jsonl")) out.push(full);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function transcriptFiles(payload) {
|
|
60
|
+
const transcript = payload.transcript_path;
|
|
61
|
+
if (typeof transcript !== "string" || !transcript) return [];
|
|
62
|
+
const files = [transcript];
|
|
63
|
+
nested(path.join(path.dirname(transcript), String(payload.session_id ?? "")), 1, files);
|
|
64
|
+
return files;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function projectOf(payload) {
|
|
68
|
+
return payload.cwd || payload.workspace?.current_dir || "";
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function labelOf(project) {
|
|
72
|
+
return project ? path.basename(project) : "session";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function settingsFor(project) {
|
|
76
|
+
const config = loadConfig();
|
|
77
|
+
return {
|
|
78
|
+
preserve: config.preserveFor?.[project] ?? config.preserveFor?.default ?? [],
|
|
79
|
+
policy: policyFor(config, project),
|
|
80
|
+
custom: config.customAdvice?.[project] ?? config.customAdvice?.default ?? "",
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function guidance(id, project, now) {
|
|
85
|
+
const plan = schedule(ADAPTER, now);
|
|
86
|
+
const view = viewFor(plan, id);
|
|
87
|
+
if (!view) return "";
|
|
88
|
+
const { preserve, policy, custom } = settingsFor(project);
|
|
89
|
+
const stage = stageFor(view.pressure.value, policy);
|
|
90
|
+
if (stage === 0) return "";
|
|
91
|
+
const advice = view.claimant.advice ?? { stage: 0, window: 0 };
|
|
92
|
+
if (advice.window === plan.windowId && advice.stage >= stage) return "";
|
|
93
|
+
upsertClaimant(ADAPTER, id, { advice: { stage, at: now, window: plan.windowId } });
|
|
94
|
+
return adviceFor(stage, {
|
|
95
|
+
target: view.allocation.target,
|
|
96
|
+
observed: view.observed,
|
|
97
|
+
pressure: view.pressure.value,
|
|
98
|
+
basis: view.pressure.basis,
|
|
99
|
+
preserve,
|
|
100
|
+
policy,
|
|
101
|
+
custom,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function selfContained(prompt) {
|
|
106
|
+
const text = String(prompt || "").trim();
|
|
107
|
+
return text.length >= 40 && !ANAPHORIC.test(text);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function tailContext(file) {
|
|
111
|
+
let fd;
|
|
112
|
+
try {
|
|
113
|
+
fd = fs.openSync(file, "r");
|
|
114
|
+
} catch {
|
|
115
|
+
return 0;
|
|
116
|
+
}
|
|
117
|
+
try {
|
|
118
|
+
const size = fs.fstatSync(fd).size;
|
|
119
|
+
const start = Math.max(0, size - NUDGE_TAIL_BYTES);
|
|
120
|
+
const buffer = Buffer.alloc(size - start);
|
|
121
|
+
fs.readSync(fd, buffer, 0, buffer.length, start);
|
|
122
|
+
const lines = buffer.toString("utf8").split("\n");
|
|
123
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
124
|
+
const line = lines[i];
|
|
125
|
+
if (!line || !line.includes('"usage"')) continue;
|
|
126
|
+
let record;
|
|
127
|
+
try {
|
|
128
|
+
record = JSON.parse(line);
|
|
129
|
+
} catch {
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
const usage = record?.message?.usage;
|
|
133
|
+
if (!usage) continue;
|
|
134
|
+
const total =
|
|
135
|
+
(usage.input_tokens || 0) + (usage.cache_read_input_tokens || 0) + (usage.cache_creation_input_tokens || 0);
|
|
136
|
+
if (total > 0) return total;
|
|
137
|
+
}
|
|
138
|
+
return 0;
|
|
139
|
+
} catch {
|
|
140
|
+
return 0;
|
|
141
|
+
} finally {
|
|
142
|
+
try {
|
|
143
|
+
fs.closeSync(fd);
|
|
144
|
+
} catch {}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function deadCarryNudge(payload, now) {
|
|
149
|
+
if (!selfContained(payload.prompt)) return "";
|
|
150
|
+
const transcript = payload.transcript_path;
|
|
151
|
+
if (typeof transcript !== "string" || !fs.existsSync(transcript)) return "";
|
|
152
|
+
const context = tailContext(transcript);
|
|
153
|
+
if (context < NUDGE_THRESHOLD) return "";
|
|
154
|
+
|
|
155
|
+
const state = readJson(NUDGE_FILE, null);
|
|
156
|
+
const events = Array.isArray(state?.events) ? state.events : [];
|
|
157
|
+
const previous = events.filter((event) => event.session === payload.session_id).pop();
|
|
158
|
+
if (previous && now - previous.at < NUDGE_COOLDOWN_MS) return "";
|
|
159
|
+
|
|
160
|
+
const horizon = context * CACHE_READ_WEIGHT * RATE_PER_TOKEN * NUDGE_HORIZON_TURNS;
|
|
161
|
+
events.push({ at: now, session: payload.session_id, context, usd: horizon });
|
|
162
|
+
writeJson(NUDGE_FILE, { installedAt: state?.installedAt ?? now, events: events.slice(-500) });
|
|
163
|
+
|
|
164
|
+
return `[savemytokens] This reads as a new task and ${Math.round(context / 1000)}k tokens of earlier work are still in context, about $${horizon.toFixed(2)} per ${NUDGE_HORIZON_TURNS} turns from here. Open your reply with one short line saying so, and that /clear or a fresh session drops it. Then do what was asked.`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function run() {
|
|
168
|
+
const event = process.argv[2] ?? "";
|
|
169
|
+
const payload = readInput();
|
|
170
|
+
if (!payload || !payload.session_id) return;
|
|
171
|
+
|
|
172
|
+
const now = Date.now();
|
|
173
|
+
const id = String(payload.session_id);
|
|
174
|
+
const project = projectOf(payload);
|
|
175
|
+
const files = transcriptFiles(payload);
|
|
176
|
+
const lines = [];
|
|
177
|
+
|
|
178
|
+
if (event === "session-end") {
|
|
179
|
+
if (files.length > 0) sampleFiles(ADAPTER, id, files, now);
|
|
180
|
+
const { defers } = consumeSignal(ADAPTER, id);
|
|
181
|
+
addDeferred(ADAPTER, project || loadMeter(ADAPTER, id).project, defers, id, now);
|
|
182
|
+
upsertClaimant(ADAPTER, id, { state: "done", endedAt: now, signal: "SESSION_END" });
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
upsertClaimant(ADAPTER, id, {
|
|
187
|
+
project,
|
|
188
|
+
label: labelOf(project),
|
|
189
|
+
...(event === "session-start" ? { state: "active", endedAt: null, signal: null } : {}),
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
if (files.length > 0) sampleFiles(ADAPTER, id, files, now);
|
|
193
|
+
|
|
194
|
+
if (event === "session-start") {
|
|
195
|
+
const view = viewFor(schedule(ADAPTER, now), id);
|
|
196
|
+
const { preserve, policy } = settingsFor(project);
|
|
197
|
+
if (policy.name !== "off") {
|
|
198
|
+
lines.push(openingAdvice({ target: view ? view.allocation.target : 1, preserve, policy }));
|
|
199
|
+
}
|
|
200
|
+
const deferred = loadDeferred(ADAPTER, project, now);
|
|
201
|
+
if (deferred.length > 0) lines.push(deferredAdvice(deferred));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (event === "prompt") {
|
|
205
|
+
const record = loadMeter(ADAPTER, id);
|
|
206
|
+
const view = viewFor(schedule(ADAPTER, now), id);
|
|
207
|
+
upsertClaimant(ADAPTER, id, {
|
|
208
|
+
prompt: String(payload.prompt ?? record.prompt ?? "").replace(/\s+/g, " ").trim().slice(0, 120),
|
|
209
|
+
...(view && (view.claimant.state !== "active" || view.claimant.parked)
|
|
210
|
+
? { state: "active", endedAt: null, signal: null, parked: false }
|
|
211
|
+
: {}),
|
|
212
|
+
});
|
|
213
|
+
const advice = guidance(id, project, now);
|
|
214
|
+
if (advice) lines.push(advice);
|
|
215
|
+
const nudge = deadCarryNudge(payload, now);
|
|
216
|
+
if (nudge) lines.push(nudge);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (event === "stop") {
|
|
220
|
+
const { signal, defers } = consumeSignal(ADAPTER, id);
|
|
221
|
+
addDeferred(ADAPTER, project || loadMeter(ADAPTER, id).project, defers, id, now);
|
|
222
|
+
if (signal === "DONE") upsertClaimant(ADAPTER, id, { state: "done", signal, endedAt: now });
|
|
223
|
+
else if (signal === "NEEDS_MORE") upsertClaimant(ADAPTER, id, { state: "needs-more", signal });
|
|
224
|
+
else if (signal === "BLOCKED") upsertClaimant(ADAPTER, id, { state: "blocked", signal });
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (lines.length > 0) process.stdout.write(lines.join("\n\n") + "\n");
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
try {
|
|
232
|
+
run();
|
|
233
|
+
} catch {}
|
|
234
|
+
process.exit(0);
|