@chroxify/eunoe 1.2.0 → 1.2.1
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/package.json +1 -1
- package/src/cli/commands/replay.ts +6 -6
- package/src/cli/commands/stats.ts +44 -13
- package/src/cli/proxy/stats.ts +48 -0
- package/src/cli/replay/index.ts +5 -3
package/package.json
CHANGED
|
@@ -200,24 +200,24 @@ export const replay: Command = {
|
|
|
200
200
|
["Cost", `${c.green(c.bold(`${fewer(sum.plainCost, sum.eunoeCost)} cheaper`))} ${c.dim("cache reads at 0.1×, writes at 1.25×")}`],
|
|
201
201
|
["Folds", c.bold(sum.folds.toLocaleString())],
|
|
202
202
|
["Folds dropped", `${c.bold(sum.dropped.toLocaleString())} ${c.dim("folded turns removed entirely to fit the window")}`],
|
|
203
|
-
["Peak context", `plain ${tokens(sum.peakPlain)} ${c.dim("→")} eunoe ${c.bold(tokens(sum.peakEunoe))}`],
|
|
203
|
+
["Peak context", `plain ${c.green(c.bold(tokens(sum.peakPlain)))} ${c.dim("→")} eunoe ${c.bold(tokens(sum.peakEunoe))} ${c.dim("· the longest conversation whole, every compaction undone, against the most eunoe sent")}`],
|
|
204
204
|
],
|
|
205
205
|
4,
|
|
206
206
|
)
|
|
207
207
|
const saved = [...compared]
|
|
208
208
|
.filter((r) => r.nativeCompactions > 0)
|
|
209
|
-
.sort((a, b) => b.
|
|
209
|
+
.sort((a, b) => b.peakPlain - a.peakPlain)
|
|
210
210
|
.slice(0, TOP_SESSIONS)
|
|
211
211
|
if (saved.length > 0) {
|
|
212
212
|
line()
|
|
213
|
-
line(` ${c.dim("
|
|
213
|
+
line(` ${c.dim("Longest sessions Claude Code would have compacted, by their whole history")}`)
|
|
214
214
|
table(
|
|
215
215
|
saved.map((r) => [
|
|
216
216
|
c.cyan(shortId(r.id)),
|
|
217
217
|
(r.title ?? (r.cwd ? tilde(r.cwd) : "")).slice(0, 40),
|
|
218
|
-
c.
|
|
219
|
-
`${
|
|
220
|
-
c.dim(
|
|
218
|
+
`${c.green(c.bold(tokens(r.peakPlain)))} ${c.dim("of history")}`,
|
|
219
|
+
c.bold(`${plural(r.nativeCompactions, "compaction")} avoided`),
|
|
220
|
+
c.dim(`eunoe peak ${tokens(r.peakEunoe)} · ${plural(r.folds, "fold")}`),
|
|
221
221
|
]),
|
|
222
222
|
)
|
|
223
223
|
}
|
|
@@ -2,7 +2,9 @@ import { claudeConfigDirs, readConfig } from "../../config"
|
|
|
2
2
|
import type { Command } from "../command"
|
|
3
3
|
import { cleanupDays } from "../proxy/claude-settings"
|
|
4
4
|
import { type RequestRecord, recentRequests } from "../proxy/inspect"
|
|
5
|
-
import { savedTokens, totals } from "../proxy/stats"
|
|
5
|
+
import { nativeCompactions, savedTokens, type ThreadPeak, threadPeaks, totals } from "../proxy/stats"
|
|
6
|
+
import { REPLAY_NATIVE_COMPACT_AT } from "../replay"
|
|
7
|
+
import { shortId } from "../sessions"
|
|
6
8
|
import { c, fail, line, percent, plural, table, tokens, wrap } from "../terminal/ui"
|
|
7
9
|
import { costOf, ratesOf, readUsage, type UsageEntry, usageFiles } from "../usage"
|
|
8
10
|
|
|
@@ -20,6 +22,7 @@ const MATCH_WITHIN = 10 * 60_000
|
|
|
20
22
|
const NOTE_WIDTH = 110
|
|
21
23
|
const DEFAULT_DAYS = 7
|
|
22
24
|
const DEFAULT_WEEKS = 8
|
|
25
|
+
const LONGEST = 5
|
|
23
26
|
|
|
24
27
|
const midnight = (ms: number) => new Date(new Date(ms).toDateString()).getTime()
|
|
25
28
|
const addDays = (ms: number, days: number) => {
|
|
@@ -123,7 +126,13 @@ function notSentCell(logged: RequestRecord[]) {
|
|
|
123
126
|
const sum = totals(logged)
|
|
124
127
|
if (sum.requests === 0) return c.dim("–")
|
|
125
128
|
if (sum.saved <= 0) return c.dim("±0")
|
|
126
|
-
return `${tokens(sum.saved)} ${c.dim(plural(sum.folds, "fold"))}`
|
|
129
|
+
return `${c.green(c.bold(tokens(sum.saved)))} ${c.dim(plural(sum.folds, "fold"))}`
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function avoidedCell(logged: RequestRecord[], avoided: Set<RequestRecord>) {
|
|
133
|
+
if (!logged.some((record) => record.input)) return c.dim("–")
|
|
134
|
+
const count = logged.filter((record) => avoided.has(record)).length
|
|
135
|
+
return count > 0 ? c.green(c.bold(count.toLocaleString())) : c.dim("0")
|
|
127
136
|
}
|
|
128
137
|
|
|
129
138
|
function costSavedCell(logged: RequestRecord[], matched: Matched) {
|
|
@@ -131,13 +140,13 @@ function costSavedCell(logged: RequestRecord[], matched: Matched) {
|
|
|
131
140
|
if (sum.priced === 0) return c.dim("–")
|
|
132
141
|
if (Math.abs(sum.saved) < 0.005) return c.dim("±0")
|
|
133
142
|
const share = c.dim(percent(sum.saved / (sum.actual + sum.saved)))
|
|
134
|
-
return sum.saved > 0 ? `${
|
|
143
|
+
return sum.saved > 0 ? `${dollars(sum.saved)} ${share}` : `${c.yellow(`−${dollars(-sum.saved)}`)} ${share}`
|
|
135
144
|
}
|
|
136
145
|
|
|
137
|
-
function row(label: string, usage: UsageEntry[], logged: RequestRecord[], matched: Matched, bold = false) {
|
|
146
|
+
function row(label: string, usage: UsageEntry[], logged: RequestRecord[], matched: Matched, avoided: Set<RequestRecord>, bold = false) {
|
|
138
147
|
const sum = summarize(usage)
|
|
139
148
|
const strong = (text: string) => (bold ? c.bold(text) : text)
|
|
140
|
-
const eunoe = [notSentCell(logged), costSavedCell(logged, matched)]
|
|
149
|
+
const eunoe = [avoidedCell(logged, avoided), notSentCell(logged), costSavedCell(logged, matched)]
|
|
141
150
|
if (sum.requests === 0) return [strong(label), c.dim("–"), "", "", "", "", "", ...eunoe]
|
|
142
151
|
return [strong(label), strong(sum.requests.toLocaleString()), tokens(sum.input), tokens(sum.output), tokens(sum.cacheWrite), tokens(sum.cacheRead), strong(`${dollars(sum.cost)}${sum.unpriced ? c.dim("*") : ""}`), ...eunoe]
|
|
143
152
|
}
|
|
@@ -161,6 +170,15 @@ function byModel(usage: UsageEntry[]) {
|
|
|
161
170
|
])
|
|
162
171
|
}
|
|
163
172
|
|
|
173
|
+
function longestRow(peak: ThreadPeak) {
|
|
174
|
+
return [
|
|
175
|
+
c.cyan(shortId(peak.session)),
|
|
176
|
+
`${c.green(c.bold(tokens(peak.plain)))} ${c.dim("of history")}`,
|
|
177
|
+
peak.compactions > 0 ? `${c.bold(plural(peak.compactions, "compaction"))} avoided` : c.dim("no compaction needed"),
|
|
178
|
+
c.dim(`eunoe sent ${tokens(peak.sent)}${peak.turns ? ` · ${plural(peak.turns, "turn")}` : ""} · ${dayLabel(Date.parse(peak.lastSeen))}`),
|
|
179
|
+
]
|
|
180
|
+
}
|
|
181
|
+
|
|
164
182
|
const countArg = (args: string[], flag: string, fallback: number) => {
|
|
165
183
|
const at = args.indexOf(flag)
|
|
166
184
|
if (at === -1) return fallback
|
|
@@ -175,9 +193,10 @@ export const stats: Command = {
|
|
|
175
193
|
details: [
|
|
176
194
|
"Reads the usage the API returned for every request in your Claude Code",
|
|
177
195
|
"sessions, subagents included, counting each response once. Cost is at",
|
|
178
|
-
"API list prices. The eunoe columns come from its request log: the
|
|
179
|
-
"
|
|
180
|
-
"
|
|
196
|
+
"API list prices. The eunoe columns come from its request log: the",
|
|
197
|
+
"compactions Claude Code would have run on the conversations it handled,",
|
|
198
|
+
"the input tokens it kept out, and what that saved at cache-read prices",
|
|
199
|
+
"once the cache writes its folds caused are paid for.",
|
|
181
200
|
],
|
|
182
201
|
options: [
|
|
183
202
|
["--days <n>", `Days to show (default ${DEFAULT_DAYS})`],
|
|
@@ -192,7 +211,9 @@ export const stats: Command = {
|
|
|
192
211
|
const since = list[0].start
|
|
193
212
|
const configDirs = claudeConfigDirs(readConfig())
|
|
194
213
|
const usage = readUsage(usageFiles(configDirs, since), since)
|
|
195
|
-
const
|
|
214
|
+
const all = recentRequests(Number.POSITIVE_INFINITY)
|
|
215
|
+
const avoided = nativeCompactions(all)
|
|
216
|
+
const logged = all.filter((record) => Date.parse(record.ts) >= since)
|
|
196
217
|
for (const entry of usage) place(list, entry.at, (bucket) => bucket.usage.push(entry))
|
|
197
218
|
const matched = matchLogged(usage, logged)
|
|
198
219
|
for (const record of logged) place(list, Date.parse(record.ts), (bucket) => bucket.logged.push(record))
|
|
@@ -201,11 +222,20 @@ export const stats: Command = {
|
|
|
201
222
|
line(` ${c.bold("eunoe stats")} ${c.dim(`the last ${count} ${weekly ? "weeks" : "days"} of Claude Code, from your session files`)}`)
|
|
202
223
|
line()
|
|
203
224
|
table([
|
|
204
|
-
[weekly ? "Week" : "Day", "Requests", "Input", "Output", "Cache write", "Cache read", "API cost", "
|
|
205
|
-
...list.map((bucket) => row(bucket.label, bucket.usage, bucket.logged, matched)),
|
|
206
|
-
row("Total", usage, logged, matched, true),
|
|
225
|
+
[weekly ? "Week" : "Day", "Requests", "Input", "Output", "Cache write", "Cache read", "API cost", "Compactions avoided", "Tokens saved", "Cost saved"].map((title) => c.dim(title)),
|
|
226
|
+
...list.map((bucket) => row(bucket.label, bucket.usage, bucket.logged, matched, avoided)),
|
|
227
|
+
row("Total", usage, logged, matched, avoided, true),
|
|
207
228
|
])
|
|
208
229
|
|
|
230
|
+
const longest = threadPeaks(all, avoided)
|
|
231
|
+
.filter((peak) => Date.parse(peak.lastSeen) >= since)
|
|
232
|
+
.slice(0, LONGEST)
|
|
233
|
+
if (longest.length > 0) {
|
|
234
|
+
line()
|
|
235
|
+
line(` ${c.dim("Longest conversations, by the history Claude Code would have had to carry")}`)
|
|
236
|
+
table(longest.map(longestRow))
|
|
237
|
+
}
|
|
238
|
+
|
|
209
239
|
const models = byModel(usage)
|
|
210
240
|
if (models.length > 0) {
|
|
211
241
|
line()
|
|
@@ -216,7 +246,8 @@ export const stats: Command = {
|
|
|
216
246
|
const handled = savings(logged, matched)
|
|
217
247
|
const notes: Array<[label: string, text: string]> = [
|
|
218
248
|
["API cost", "At list prices, including cache writes and fast mode."],
|
|
219
|
-
["
|
|
249
|
+
["Compactions", `Times Claude Code would have compacted a conversation eunoe handled, at ${Math.round(REPLAY_NATIVE_COMPACT_AT * 100)}% of its window. eunoe kept every turn instead.`],
|
|
250
|
+
["Tokens saved", "Input eunoe kept out, mostly history Claude Code would have read from cache."],
|
|
220
251
|
["Cost saved", "Those tokens at cache-read prices, minus what each fold cost to rewrite."],
|
|
221
252
|
...(handled.priced < handled.handled ? [["Not priced", `${(handled.handled - handled.priced).toLocaleString()} of ${handled.handled.toLocaleString()} eunoe requests: ${leftOut(handled)}.`] as [string, string]] : []),
|
|
222
253
|
...(summarize(usage).unpriced ? [["* No price", "Some models have no known price, so their tokens count but their cost doesn't."] as [string, string]] : []),
|
package/src/cli/proxy/stats.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { DEFAULT_RATIO } from "../../proxy/estimate"
|
|
2
|
+
import { NATIVE_SUMMARY_CHARS, REPLAY_NATIVE_COMPACT_AT } from "../replay"
|
|
1
3
|
import type { RequestRecord } from "./inspect"
|
|
2
4
|
|
|
3
5
|
export interface SessionStats {
|
|
@@ -84,3 +86,49 @@ export function savedShare(context: number, plain: number | null): number | null
|
|
|
84
86
|
if (!plain) return null
|
|
85
87
|
return plain > context ? 1 - context / plain : 0
|
|
86
88
|
}
|
|
89
|
+
|
|
90
|
+
export function nativeCompactions(records: RequestRecord[]): Set<RequestRecord> {
|
|
91
|
+
const summary = Math.round(NATIVE_SUMMARY_CHARS * DEFAULT_RATIO)
|
|
92
|
+
const offsets = new Map<string, number>()
|
|
93
|
+
const compacted = new Set<RequestRecord>()
|
|
94
|
+
for (const record of records) {
|
|
95
|
+
const plain = scaled(record, record.charsIn)
|
|
96
|
+
const window = typeof record.window === "number" ? record.window : 0
|
|
97
|
+
if (typeof record.thread !== "string" || plain === null || window <= 0) continue
|
|
98
|
+
if (plain - (offsets.get(record.thread) ?? 0) <= REPLAY_NATIVE_COMPACT_AT * window) continue
|
|
99
|
+
compacted.add(record)
|
|
100
|
+
offsets.set(record.thread, plain - (scaled(record, record.charsFixed) ?? 0) - summary)
|
|
101
|
+
}
|
|
102
|
+
return compacted
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface ThreadPeak {
|
|
106
|
+
thread: string
|
|
107
|
+
session: string
|
|
108
|
+
plain: number
|
|
109
|
+
sent: number
|
|
110
|
+
turns: number | null
|
|
111
|
+
compactions: number
|
|
112
|
+
lastSeen: string
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function threadPeaks(records: RequestRecord[], compacted: Set<RequestRecord>): ThreadPeak[] {
|
|
116
|
+
const peaks = new Map<string, ThreadPeak>()
|
|
117
|
+
for (const record of records) {
|
|
118
|
+
const plain = scaled(record, record.charsIn)
|
|
119
|
+
const session = sessionOf(record)
|
|
120
|
+
if (typeof record.thread !== "string" || !session || plain === null) continue
|
|
121
|
+
const previous = peaks.get(record.thread)
|
|
122
|
+
const higher = !previous || plain >= previous.plain
|
|
123
|
+
peaks.set(record.thread, {
|
|
124
|
+
thread: record.thread,
|
|
125
|
+
session,
|
|
126
|
+
plain: higher ? plain : previous.plain,
|
|
127
|
+
sent: higher ? inputOf(record) : previous.sent,
|
|
128
|
+
turns: record.turns ?? previous?.turns ?? null,
|
|
129
|
+
compactions: (previous?.compactions ?? 0) + (compacted.has(record) ? 1 : 0),
|
|
130
|
+
lastSeen: record.ts,
|
|
131
|
+
})
|
|
132
|
+
}
|
|
133
|
+
return [...peaks.values()].sort((a, b) => b.plain - a.plain)
|
|
134
|
+
}
|
package/src/cli/replay/index.ts
CHANGED
|
@@ -43,7 +43,7 @@ export interface ReplayFailure {
|
|
|
43
43
|
|
|
44
44
|
export const REPLAY_NATIVE_COMPACT_AT = 0.92
|
|
45
45
|
|
|
46
|
-
const NATIVE_SUMMARY_CHARS = 30_000
|
|
46
|
+
export const NATIVE_SUMMARY_CHARS = 30_000
|
|
47
47
|
const CACHE_READ_WEIGHT = 0.1
|
|
48
48
|
const CACHE_WRITE_WEIGHT = 1.25
|
|
49
49
|
|
|
@@ -85,7 +85,7 @@ export function replaySession(messages: Message[], { config, window, fixedTokens
|
|
|
85
85
|
|
|
86
86
|
const state: ThreadState = {}
|
|
87
87
|
const eunoe = { sent: [] as Message[], sum: 0, count: 0, upTo: 0, recall: 0, compactedTurn: -2, previous: 0 }
|
|
88
|
-
const plain = { sum: 0, count: 0, since: 0, previous: 0 }
|
|
88
|
+
const plain = { sum: 0, count: 0, since: 0, previous: 0, whole: 0, wholeCount: 0 }
|
|
89
89
|
|
|
90
90
|
const sendEunoe = (i: number) => {
|
|
91
91
|
const turn = turnAt[i - 1]
|
|
@@ -119,10 +119,12 @@ export function replaySession(messages: Message[], { config, window, fixedTokens
|
|
|
119
119
|
for (; plain.since < i; plain.since += 1) {
|
|
120
120
|
plain.sum += sizes[plain.since]
|
|
121
121
|
plain.count += 1
|
|
122
|
+
plain.whole += sizes[plain.since]
|
|
123
|
+
plain.wholeCount += 1
|
|
122
124
|
}
|
|
125
|
+
out.peakPlain = Math.max(out.peakPlain, tokens(fixedChars + listChars(plain.whole, plain.wholeCount)))
|
|
123
126
|
let chars = fixedChars + listChars(plain.sum, plain.count)
|
|
124
127
|
let cacheable = plain.previous
|
|
125
|
-
out.peakPlain = Math.max(out.peakPlain, tokens(chars))
|
|
126
128
|
if (tokens(chars) > REPLAY_NATIVE_COMPACT_AT * window) {
|
|
127
129
|
out.nativeCompactions += 1
|
|
128
130
|
out.discarded += tokens(chars - fixedChars)
|