ccc-notifier 0.1.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 +171 -0
- package/dist/budget-5H2GQRXH.js +71 -0
- package/dist/chunk-4JNZ7BHO.js +276 -0
- package/dist/chunk-64N5SGTT.js +1024 -0
- package/dist/chunk-DGXUSPS4.js +21 -0
- package/dist/chunk-IIYMGLV4.js +309 -0
- package/dist/chunk-KUJZYZSG.js +66 -0
- package/dist/chunk-L5MLVTA2.js +393 -0
- package/dist/chunk-OEG3AVU6.js +436 -0
- package/dist/chunk-TB5A7U7G.js +82 -0
- package/dist/chunk-TBFKGFZX.js +87 -0
- package/dist/cli.js +717 -0
- package/dist/dashboard-LKK6NOBN.js +14 -0
- package/dist/history-MWFHO5VR.js +114 -0
- package/dist/mute-2JW3NQXN.js +12 -0
- package/dist/setup-RT62VO37.js +15 -0
- package/dist/sweep-XKKODGMA.js +460 -0
- package/dist/track-PBHTVZ3L.js +168 -0
- package/package.json +53 -0
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/pricing.ts
|
|
4
|
+
import { promises as fs } from "fs";
|
|
5
|
+
import path from "path";
|
|
6
|
+
var LITELLM_URL = "https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json";
|
|
7
|
+
var LITELLM_FETCH_TIMEOUT_MS = 3e3;
|
|
8
|
+
var CACHE_FRESH_MS = 24 * 60 * 60 * 1e3;
|
|
9
|
+
function price(input, output, cacheWrite5m, cacheWrite1h, cacheRead, source) {
|
|
10
|
+
return { input, output, cacheWrite5m, cacheWrite1h, cacheRead, source };
|
|
11
|
+
}
|
|
12
|
+
function builtinPriceTable() {
|
|
13
|
+
return {
|
|
14
|
+
"claude-fable-5": price(10, 50, 12.5, 20, 1, "builtin"),
|
|
15
|
+
"claude-mythos-5": price(10, 50, 12.5, 20, 1, "builtin"),
|
|
16
|
+
"claude-opus-4-8": price(5, 25, 6.25, 10, 0.5, "builtin"),
|
|
17
|
+
"claude-opus-4-7": price(5, 25, 6.25, 10, 0.5, "builtin"),
|
|
18
|
+
"claude-opus-4-6": price(5, 25, 6.25, 10, 0.5, "builtin"),
|
|
19
|
+
"claude-opus-4-5": price(5, 25, 6.25, 10, 0.5, "builtin"),
|
|
20
|
+
"claude-opus-4-1": price(15, 75, 18.75, 30, 1.5, "builtin"),
|
|
21
|
+
"claude-opus-4": price(15, 75, 18.75, 30, 1.5, "builtin"),
|
|
22
|
+
// 旧 claude-opus-4-20250514 の受け皿
|
|
23
|
+
"claude-3-opus": price(15, 75, 18.75, 30, 1.5, "builtin"),
|
|
24
|
+
"claude-sonnet-5": price(3, 15, 3.75, 6, 0.3, "builtin"),
|
|
25
|
+
"claude-sonnet-4-6": price(3, 15, 3.75, 6, 0.3, "builtin"),
|
|
26
|
+
"claude-sonnet-4-5": price(3, 15, 3.75, 6, 0.3, "builtin"),
|
|
27
|
+
"claude-sonnet-4": price(3, 15, 3.75, 6, 0.3, "builtin"),
|
|
28
|
+
"claude-3-7-sonnet": price(3, 15, 3.75, 6, 0.3, "builtin"),
|
|
29
|
+
"claude-3-5-sonnet": price(3, 15, 3.75, 6, 0.3, "builtin"),
|
|
30
|
+
"claude-haiku-4-5": price(1, 5, 1.25, 2, 0.1, "builtin"),
|
|
31
|
+
"claude-3-5-haiku": price(0.8, 4, 1, 1.6, 0.08, "builtin"),
|
|
32
|
+
"claude-3-haiku": price(0.25, 1.25, 0.3125, 0.5, 0.025, "builtin")
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function normalizeModelId(modelId) {
|
|
36
|
+
let s = modelId.toLowerCase();
|
|
37
|
+
s = s.replace(/^anthropic[\/.]/, "");
|
|
38
|
+
s = s.replace(/\[1m\]$/, "");
|
|
39
|
+
s = s.replace(/-20\d{6}$/, "");
|
|
40
|
+
return s.trim();
|
|
41
|
+
}
|
|
42
|
+
function resolvePrice(modelId, table) {
|
|
43
|
+
const target = normalizeModelId(modelId);
|
|
44
|
+
let bestKeyLen = -1;
|
|
45
|
+
let bestPrice = null;
|
|
46
|
+
for (const rawKey of Object.keys(table)) {
|
|
47
|
+
const key = normalizeModelId(rawKey);
|
|
48
|
+
if (key.length === 0 || !target.startsWith(key)) continue;
|
|
49
|
+
if (key.length > bestKeyLen) {
|
|
50
|
+
bestKeyLen = key.length;
|
|
51
|
+
bestPrice = table[rawKey];
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return bestPrice ? { ...bestPrice } : null;
|
|
55
|
+
}
|
|
56
|
+
function computeCost(main, sidechain, table) {
|
|
57
|
+
const byModel = {};
|
|
58
|
+
const unknownModels = [];
|
|
59
|
+
let usd = 0;
|
|
60
|
+
const accumulate = (usage) => {
|
|
61
|
+
for (const [model, tokens] of Object.entries(usage)) {
|
|
62
|
+
const p = resolvePrice(model, table);
|
|
63
|
+
let cost = 0;
|
|
64
|
+
if (p === null) {
|
|
65
|
+
if (!unknownModels.includes(model)) unknownModels.push(model);
|
|
66
|
+
} else {
|
|
67
|
+
cost = (tokens.input * p.input + tokens.output * p.output + tokens.cacheWrite5m * p.cacheWrite5m + tokens.cacheWrite1h * p.cacheWrite1h + tokens.cacheRead * p.cacheRead) / 1e6;
|
|
68
|
+
}
|
|
69
|
+
byModel[model] = (byModel[model] ?? 0) + cost;
|
|
70
|
+
usd += cost;
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
accumulate(main);
|
|
74
|
+
accumulate(sidechain);
|
|
75
|
+
return { usd, byModel, unknownModels };
|
|
76
|
+
}
|
|
77
|
+
function cacheFilePath(cacheDir) {
|
|
78
|
+
return path.join(cacheDir, "pricing.json");
|
|
79
|
+
}
|
|
80
|
+
async function readPriceCache(cacheDir) {
|
|
81
|
+
try {
|
|
82
|
+
const raw = await fs.readFile(cacheFilePath(cacheDir), "utf8");
|
|
83
|
+
const parsed = JSON.parse(raw);
|
|
84
|
+
if (parsed !== null && typeof parsed === "object" && typeof parsed.fetchedAt === "string" && typeof parsed.table === "object" && parsed.table !== null) {
|
|
85
|
+
const p = parsed;
|
|
86
|
+
return { fetchedAt: p.fetchedAt, table: p.table };
|
|
87
|
+
}
|
|
88
|
+
return null;
|
|
89
|
+
} catch {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
async function writePriceCache(cacheDir, table) {
|
|
94
|
+
const file = cacheFilePath(cacheDir);
|
|
95
|
+
const payload = { fetchedAt: (/* @__PURE__ */ new Date()).toISOString(), table };
|
|
96
|
+
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
97
|
+
await fs.writeFile(file, JSON.stringify(payload, null, 2), "utf8");
|
|
98
|
+
}
|
|
99
|
+
function isCacheFresh(fetchedAt) {
|
|
100
|
+
const t = Date.parse(fetchedAt);
|
|
101
|
+
if (Number.isNaN(t)) return false;
|
|
102
|
+
return Date.now() - t <= CACHE_FRESH_MS;
|
|
103
|
+
}
|
|
104
|
+
function toFiniteNumber(v) {
|
|
105
|
+
return typeof v === "number" && Number.isFinite(v) ? v : null;
|
|
106
|
+
}
|
|
107
|
+
function convertLiteLLMPayload(payload) {
|
|
108
|
+
if (payload === null || typeof payload !== "object") {
|
|
109
|
+
throw new Error("invalid litellm payload: not an object");
|
|
110
|
+
}
|
|
111
|
+
const table = {};
|
|
112
|
+
for (const [rawKey, rawEntry] of Object.entries(payload)) {
|
|
113
|
+
if (rawEntry === null || typeof rawEntry !== "object") continue;
|
|
114
|
+
const entry = rawEntry;
|
|
115
|
+
const provider = entry.litellm_provider;
|
|
116
|
+
if (typeof provider === "string" && provider !== "anthropic") continue;
|
|
117
|
+
let key = rawKey.toLowerCase();
|
|
118
|
+
if (key.startsWith("anthropic/")) key = key.slice("anthropic/".length);
|
|
119
|
+
if (!key.startsWith("claude")) continue;
|
|
120
|
+
const inputRaw = toFiniteNumber(entry.input_cost_per_token);
|
|
121
|
+
const outputRaw = toFiniteNumber(entry.output_cost_per_token);
|
|
122
|
+
if (inputRaw === null || inputRaw <= 0) continue;
|
|
123
|
+
if (outputRaw === null || outputRaw <= 0) continue;
|
|
124
|
+
const input = inputRaw * 1e6;
|
|
125
|
+
const output = outputRaw * 1e6;
|
|
126
|
+
const cacheReadRaw = toFiniteNumber(entry.cache_read_input_token_cost);
|
|
127
|
+
const cacheWrite5mRaw = toFiniteNumber(entry.cache_creation_input_token_cost);
|
|
128
|
+
const cacheWrite1hRaw = toFiniteNumber(entry.cache_creation_input_token_cost_above_1hr);
|
|
129
|
+
table[key] = {
|
|
130
|
+
input,
|
|
131
|
+
output,
|
|
132
|
+
cacheRead: cacheReadRaw !== null ? cacheReadRaw * 1e6 : input * 0.1,
|
|
133
|
+
cacheWrite5m: cacheWrite5mRaw !== null ? cacheWrite5mRaw * 1e6 : input * 1.25,
|
|
134
|
+
cacheWrite1h: cacheWrite1hRaw !== null ? cacheWrite1hRaw * 1e6 : input * 2,
|
|
135
|
+
source: "litellm"
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
return table;
|
|
139
|
+
}
|
|
140
|
+
async function fetchLiteLLMPriceTable() {
|
|
141
|
+
const controller = new AbortController();
|
|
142
|
+
const timer = setTimeout(() => controller.abort(), LITELLM_FETCH_TIMEOUT_MS);
|
|
143
|
+
try {
|
|
144
|
+
const res = await fetch(LITELLM_URL, { signal: controller.signal });
|
|
145
|
+
if (!res.ok) {
|
|
146
|
+
throw new Error(`litellm fetch failed with status ${res.status}`);
|
|
147
|
+
}
|
|
148
|
+
const json = await res.json();
|
|
149
|
+
return convertLiteLLMPayload(json);
|
|
150
|
+
} finally {
|
|
151
|
+
clearTimeout(timer);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
async function loadPriceTable(cacheDir, opts) {
|
|
155
|
+
const builtin = builtinPriceTable();
|
|
156
|
+
const cached = await readPriceCache(cacheDir);
|
|
157
|
+
if (cached !== null && isCacheFresh(cached.fetchedAt)) {
|
|
158
|
+
return { ...builtin, ...cached.table };
|
|
159
|
+
}
|
|
160
|
+
if (opts?.offline === true) {
|
|
161
|
+
return cached !== null ? { ...builtin, ...cached.table } : builtin;
|
|
162
|
+
}
|
|
163
|
+
try {
|
|
164
|
+
const remoteTable = await fetchLiteLLMPriceTable();
|
|
165
|
+
await writePriceCache(cacheDir, remoteTable);
|
|
166
|
+
return { ...builtin, ...remoteTable };
|
|
167
|
+
} catch {
|
|
168
|
+
return cached !== null ? { ...builtin, ...cached.table } : builtin;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// src/fx.ts
|
|
173
|
+
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
174
|
+
import { join } from "path";
|
|
175
|
+
var FETCH_TIMEOUT_MS = 1500;
|
|
176
|
+
var CACHE_FILE_NAME = "fx.json";
|
|
177
|
+
var FX_SOURCES = [
|
|
178
|
+
"https://api.frankfurter.dev/v1/latest?base=USD&symbols=JPY",
|
|
179
|
+
"https://open.er-api.com/v6/latest/USD"
|
|
180
|
+
];
|
|
181
|
+
function cacheFilePath2(cacheDir) {
|
|
182
|
+
return join(cacheDir, CACHE_FILE_NAME);
|
|
183
|
+
}
|
|
184
|
+
function isPositiveFiniteNumber(v) {
|
|
185
|
+
return typeof v === "number" && Number.isFinite(v) && v > 0;
|
|
186
|
+
}
|
|
187
|
+
function parseFxCache(raw) {
|
|
188
|
+
if (typeof raw !== "object" || raw === null) return null;
|
|
189
|
+
const obj = raw;
|
|
190
|
+
if (!isPositiveFiniteNumber(obj.rate)) return null;
|
|
191
|
+
if (typeof obj.fetchedAt !== "string") return null;
|
|
192
|
+
return { rate: obj.rate, fetchedAt: obj.fetchedAt };
|
|
193
|
+
}
|
|
194
|
+
async function readFxCache(cacheDir) {
|
|
195
|
+
try {
|
|
196
|
+
const raw = await readFile(cacheFilePath2(cacheDir), "utf8");
|
|
197
|
+
return parseFxCache(JSON.parse(raw));
|
|
198
|
+
} catch {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
async function writeFxCache(cacheDir, cache) {
|
|
203
|
+
try {
|
|
204
|
+
await mkdir(cacheDir, { recursive: true });
|
|
205
|
+
await writeFile(cacheFilePath2(cacheDir), JSON.stringify(cache), "utf8");
|
|
206
|
+
} catch {
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
function isFresh(fetchedAt, cacheHours) {
|
|
210
|
+
const fetchedMs = Date.parse(fetchedAt);
|
|
211
|
+
if (Number.isNaN(fetchedMs)) return false;
|
|
212
|
+
const ageMs = Date.now() - fetchedMs;
|
|
213
|
+
return ageMs <= cacheHours * 60 * 60 * 1e3;
|
|
214
|
+
}
|
|
215
|
+
function extractJpyRate(json) {
|
|
216
|
+
if (typeof json !== "object" || json === null) return null;
|
|
217
|
+
const rates = json.rates;
|
|
218
|
+
if (typeof rates !== "object" || rates === null) return null;
|
|
219
|
+
const jpy = rates.JPY;
|
|
220
|
+
return isPositiveFiniteNumber(jpy) ? jpy : null;
|
|
221
|
+
}
|
|
222
|
+
async function fetchJpyRate(url) {
|
|
223
|
+
const controller = new AbortController();
|
|
224
|
+
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
225
|
+
try {
|
|
226
|
+
const res = await fetch(url, { signal: controller.signal });
|
|
227
|
+
const json = await res.json();
|
|
228
|
+
return extractJpyRate(json);
|
|
229
|
+
} catch {
|
|
230
|
+
return null;
|
|
231
|
+
} finally {
|
|
232
|
+
clearTimeout(timer);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
async function getUsdJpy(cfg, cacheDir) {
|
|
236
|
+
const cache = await readFxCache(cacheDir);
|
|
237
|
+
if (cache && isFresh(cache.fetchedAt, cfg.fx.cacheHours)) {
|
|
238
|
+
return { rate: cache.rate, source: "cache", fetchedAt: cache.fetchedAt };
|
|
239
|
+
}
|
|
240
|
+
for (const url of FX_SOURCES) {
|
|
241
|
+
const rate = await fetchJpyRate(url);
|
|
242
|
+
if (rate !== null) {
|
|
243
|
+
const fetchedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
244
|
+
await writeFxCache(cacheDir, { rate, fetchedAt });
|
|
245
|
+
return { rate, source: "live", fetchedAt };
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
if (cache) {
|
|
249
|
+
return { rate: cache.rate, source: "cache", fetchedAt: cache.fetchedAt };
|
|
250
|
+
}
|
|
251
|
+
return { rate: cfg.fx.fallbackRate, source: "fixed", fetchedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// src/transcript.ts
|
|
255
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
256
|
+
var NEWLINE = 10;
|
|
257
|
+
var MAX_SEEN_KEYS = 500;
|
|
258
|
+
var SYNTHETIC_MODEL = "<synthetic>";
|
|
259
|
+
function isRecord(v) {
|
|
260
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
261
|
+
}
|
|
262
|
+
function numOf(v) {
|
|
263
|
+
return typeof v === "number" && Number.isFinite(v) ? v : 0;
|
|
264
|
+
}
|
|
265
|
+
function strOrNull(v) {
|
|
266
|
+
return typeof v === "string" ? v : null;
|
|
267
|
+
}
|
|
268
|
+
function emptyBuckets() {
|
|
269
|
+
return { input: 0, output: 0, cacheWrite5m: 0, cacheWrite1h: 0, cacheRead: 0 };
|
|
270
|
+
}
|
|
271
|
+
function addToModel(target, model, b) {
|
|
272
|
+
const cur = target[model] ?? emptyBuckets();
|
|
273
|
+
cur.input += b.input;
|
|
274
|
+
cur.output += b.output;
|
|
275
|
+
cur.cacheWrite5m += b.cacheWrite5m;
|
|
276
|
+
cur.cacheWrite1h += b.cacheWrite1h;
|
|
277
|
+
cur.cacheRead += b.cacheRead;
|
|
278
|
+
target[model] = cur;
|
|
279
|
+
}
|
|
280
|
+
function extractBucket(usage) {
|
|
281
|
+
const input = numOf(usage.input_tokens);
|
|
282
|
+
const output = numOf(usage.output_tokens);
|
|
283
|
+
const cacheRead = numOf(usage.cache_read_input_tokens);
|
|
284
|
+
let cacheWrite5m;
|
|
285
|
+
let cacheWrite1h;
|
|
286
|
+
const cc = usage.cache_creation;
|
|
287
|
+
if (isRecord(cc)) {
|
|
288
|
+
cacheWrite5m = numOf(cc.ephemeral_5m_input_tokens);
|
|
289
|
+
cacheWrite1h = numOf(cc.ephemeral_1h_input_tokens);
|
|
290
|
+
} else {
|
|
291
|
+
cacheWrite5m = numOf(usage.cache_creation_input_tokens);
|
|
292
|
+
cacheWrite1h = 0;
|
|
293
|
+
}
|
|
294
|
+
return { input, output, cacheWrite5m, cacheWrite1h, cacheRead };
|
|
295
|
+
}
|
|
296
|
+
function promptCandidate(content) {
|
|
297
|
+
if (typeof content === "string") return content;
|
|
298
|
+
if (Array.isArray(content)) {
|
|
299
|
+
let hasToolResult = false;
|
|
300
|
+
const texts = [];
|
|
301
|
+
for (const block of content) {
|
|
302
|
+
if (!isRecord(block)) continue;
|
|
303
|
+
if (block.type === "tool_result") hasToolResult = true;
|
|
304
|
+
else if (block.type === "text" && typeof block.text === "string") texts.push(block.text);
|
|
305
|
+
}
|
|
306
|
+
if (hasToolResult) return null;
|
|
307
|
+
return texts.join("\n");
|
|
308
|
+
}
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
async function readAll(path2) {
|
|
312
|
+
try {
|
|
313
|
+
return await readFile2(path2);
|
|
314
|
+
} catch {
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
async function aggregateNewTurn(transcriptPath, cursor) {
|
|
319
|
+
const buffer = await readAll(transcriptPath);
|
|
320
|
+
if (buffer === null) return null;
|
|
321
|
+
const fileSize = buffer.length;
|
|
322
|
+
let startOffset;
|
|
323
|
+
let rescan;
|
|
324
|
+
if (cursor !== null && cursor.offset > 0 && cursor.offset <= fileSize && buffer[cursor.offset - 1] === NEWLINE) {
|
|
325
|
+
startOffset = cursor.offset;
|
|
326
|
+
rescan = false;
|
|
327
|
+
} else {
|
|
328
|
+
startOffset = 0;
|
|
329
|
+
rescan = cursor !== null;
|
|
330
|
+
}
|
|
331
|
+
const seenKeys = new Set(cursor?.seenMessageKeys ?? []);
|
|
332
|
+
const tsFloor = cursor?.lastTs ?? null;
|
|
333
|
+
const pending = /* @__PURE__ */ new Map();
|
|
334
|
+
let sessionId = "";
|
|
335
|
+
let cwd = null;
|
|
336
|
+
let gitBranch = null;
|
|
337
|
+
let firstTs = null;
|
|
338
|
+
let lastTs = null;
|
|
339
|
+
let lastUuid = null;
|
|
340
|
+
let prompt = null;
|
|
341
|
+
const handleLine = (raw) => {
|
|
342
|
+
if (raw.trim().length === 0) return;
|
|
343
|
+
let obj;
|
|
344
|
+
try {
|
|
345
|
+
obj = JSON.parse(raw);
|
|
346
|
+
} catch {
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
if (!isRecord(obj)) return;
|
|
350
|
+
const ts = strOrNull(obj.timestamp);
|
|
351
|
+
if (rescan && tsFloor !== null && ts !== null && ts <= tsFloor) return;
|
|
352
|
+
const isSide = obj.isSidechain === true;
|
|
353
|
+
const sid = strOrNull(obj.sessionId);
|
|
354
|
+
if (sid !== null) sessionId = sid;
|
|
355
|
+
if (!isSide) {
|
|
356
|
+
const c = strOrNull(obj.cwd);
|
|
357
|
+
if (c !== null) cwd = c;
|
|
358
|
+
const gb = strOrNull(obj.gitBranch);
|
|
359
|
+
if (gb !== null) gitBranch = gb;
|
|
360
|
+
}
|
|
361
|
+
if (ts !== null) {
|
|
362
|
+
if (firstTs === null || ts < firstTs) firstTs = ts;
|
|
363
|
+
if (lastTs === null || ts > lastTs) lastTs = ts;
|
|
364
|
+
}
|
|
365
|
+
const uuid = strOrNull(obj.uuid);
|
|
366
|
+
if (uuid !== null) lastUuid = uuid;
|
|
367
|
+
const type = obj.type;
|
|
368
|
+
const message = isRecord(obj.message) ? obj.message : null;
|
|
369
|
+
if (type === "user" && !isSide && message !== null) {
|
|
370
|
+
const cand = promptCandidate(message.content);
|
|
371
|
+
if (cand !== null) {
|
|
372
|
+
const t = cand.trim();
|
|
373
|
+
if (t.length > 0 && !t.startsWith("<")) prompt = t;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
if (type === "assistant" && message !== null) {
|
|
377
|
+
const usage = message.usage;
|
|
378
|
+
if (isRecord(usage)) {
|
|
379
|
+
const rawModel = message.model;
|
|
380
|
+
if (rawModel !== SYNTHETIC_MODEL) {
|
|
381
|
+
const id = strOrNull(message.id) ?? "";
|
|
382
|
+
const reqId = strOrNull(obj.requestId) ?? "";
|
|
383
|
+
const key = `${id}:${reqId}`;
|
|
384
|
+
if (!seenKeys.has(key)) {
|
|
385
|
+
const model = strOrNull(rawModel) ?? "unknown";
|
|
386
|
+
pending.set(key, { model, isSidechain: isSide, bucket: extractBucket(usage) });
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
let lineStart = startOffset;
|
|
393
|
+
for (let pos = startOffset; pos < fileSize; pos++) {
|
|
394
|
+
if (buffer[pos] !== NEWLINE) continue;
|
|
395
|
+
handleLine(buffer.toString("utf8", lineStart, pos));
|
|
396
|
+
lineStart = pos + 1;
|
|
397
|
+
}
|
|
398
|
+
const newOffset = lineStart;
|
|
399
|
+
if (pending.size === 0) return null;
|
|
400
|
+
const main = {};
|
|
401
|
+
const sidechain = {};
|
|
402
|
+
const newKeys = [];
|
|
403
|
+
for (const [key, pm] of pending) {
|
|
404
|
+
newKeys.push(key);
|
|
405
|
+
if (pm.isSidechain) addToModel(sidechain, pm.model, pm.bucket);
|
|
406
|
+
else addToModel(main, pm.model, pm.bucket);
|
|
407
|
+
}
|
|
408
|
+
const combined = [...cursor?.seenMessageKeys ?? [], ...newKeys];
|
|
409
|
+
const seenMessageKeys = combined.length > MAX_SEEN_KEYS ? combined.slice(combined.length - MAX_SEEN_KEYS) : combined;
|
|
410
|
+
return {
|
|
411
|
+
sessionId,
|
|
412
|
+
main,
|
|
413
|
+
sidechain,
|
|
414
|
+
apiCalls: pending.size,
|
|
415
|
+
prompt,
|
|
416
|
+
cwd,
|
|
417
|
+
gitBranch,
|
|
418
|
+
firstTs,
|
|
419
|
+
lastTs,
|
|
420
|
+
newCursor: {
|
|
421
|
+
offset: newOffset,
|
|
422
|
+
lastUuid,
|
|
423
|
+
lastTs,
|
|
424
|
+
seenMessageKeys
|
|
425
|
+
}
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
export {
|
|
430
|
+
computeCost,
|
|
431
|
+
loadPriceTable,
|
|
432
|
+
getUsdJpy,
|
|
433
|
+
extractBucket,
|
|
434
|
+
promptCandidate,
|
|
435
|
+
aggregateNewTurn
|
|
436
|
+
};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
aggregateNewTurn
|
|
4
|
+
} from "./chunk-OEG3AVU6.js";
|
|
5
|
+
import {
|
|
6
|
+
loadCursor,
|
|
7
|
+
logError,
|
|
8
|
+
sanitizeCursor
|
|
9
|
+
} from "./chunk-IIYMGLV4.js";
|
|
10
|
+
|
|
11
|
+
// src/subagents.ts
|
|
12
|
+
import { promises as fs } from "fs";
|
|
13
|
+
import { join } from "path";
|
|
14
|
+
var MAX_AGENT_FILES = 200;
|
|
15
|
+
function emptyBuckets() {
|
|
16
|
+
return { input: 0, output: 0, cacheWrite5m: 0, cacheWrite1h: 0, cacheRead: 0 };
|
|
17
|
+
}
|
|
18
|
+
function addToModel(target, model, b) {
|
|
19
|
+
const cur = target[model] ?? emptyBuckets();
|
|
20
|
+
cur.input += b.input;
|
|
21
|
+
cur.output += b.output;
|
|
22
|
+
cur.cacheWrite5m += b.cacheWrite5m;
|
|
23
|
+
cur.cacheWrite1h += b.cacheWrite1h;
|
|
24
|
+
cur.cacheRead += b.cacheRead;
|
|
25
|
+
target[model] = cur;
|
|
26
|
+
}
|
|
27
|
+
function mergeUsage(target, src) {
|
|
28
|
+
for (const [model, b] of Object.entries(src)) addToModel(target, model, b);
|
|
29
|
+
}
|
|
30
|
+
function subagentsDirOf(mainTranscriptPath) {
|
|
31
|
+
const base = mainTranscriptPath.endsWith(".jsonl") ? mainTranscriptPath.slice(0, -".jsonl".length) : mainTranscriptPath;
|
|
32
|
+
return join(base, "subagents");
|
|
33
|
+
}
|
|
34
|
+
async function listAgentFiles(dir, entries) {
|
|
35
|
+
const files = entries.filter((e) => e.isFile() && e.name.startsWith("agent-") && e.name.endsWith(".jsonl")).map((e) => join(dir, e.name));
|
|
36
|
+
if (files.length <= MAX_AGENT_FILES) return files;
|
|
37
|
+
const withMtime = [];
|
|
38
|
+
for (const p of files) {
|
|
39
|
+
let mtime = 0;
|
|
40
|
+
try {
|
|
41
|
+
mtime = (await fs.stat(p)).mtimeMs;
|
|
42
|
+
} catch {
|
|
43
|
+
mtime = 0;
|
|
44
|
+
}
|
|
45
|
+
withMtime.push({ path: p, mtime });
|
|
46
|
+
}
|
|
47
|
+
withMtime.sort((a, b) => b.mtime - a.mtime);
|
|
48
|
+
return withMtime.slice(0, MAX_AGENT_FILES).map((x) => x.path);
|
|
49
|
+
}
|
|
50
|
+
async function collectSubagentUsage(mainTranscriptPath) {
|
|
51
|
+
const dir = subagentsDirOf(mainTranscriptPath);
|
|
52
|
+
let entries;
|
|
53
|
+
try {
|
|
54
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
55
|
+
} catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
const files = await listAgentFiles(dir, entries);
|
|
59
|
+
const perModel = {};
|
|
60
|
+
let apiCalls = 0;
|
|
61
|
+
let agentFiles = 0;
|
|
62
|
+
const newCursors = [];
|
|
63
|
+
for (const filePath of files) {
|
|
64
|
+
try {
|
|
65
|
+
const cursor = sanitizeCursor(loadCursor(filePath));
|
|
66
|
+
const agg = await aggregateNewTurn(filePath, cursor);
|
|
67
|
+
if (agg === null) continue;
|
|
68
|
+
mergeUsage(perModel, agg.main);
|
|
69
|
+
mergeUsage(perModel, agg.sidechain);
|
|
70
|
+
apiCalls += agg.apiCalls;
|
|
71
|
+
agentFiles += 1;
|
|
72
|
+
newCursors.push({ path: filePath, cursor: agg.newCursor });
|
|
73
|
+
} catch (err) {
|
|
74
|
+
logError("subagents:file", err);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return { perModel, apiCalls, agentFiles, newCursors };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export {
|
|
81
|
+
collectSubagentUsage
|
|
82
|
+
};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/format.ts
|
|
4
|
+
import path from "path";
|
|
5
|
+
function formatUSD(n) {
|
|
6
|
+
const digits = n < 0.01 ? 4 : n < 1 ? 3 : 2;
|
|
7
|
+
return `$${n.toFixed(digits)}`;
|
|
8
|
+
}
|
|
9
|
+
function formatJPY(n) {
|
|
10
|
+
if (n < 1) {
|
|
11
|
+
return `\xA5${n.toFixed(1)}`;
|
|
12
|
+
}
|
|
13
|
+
return `\xA5${groupThousands(Math.round(n))}`;
|
|
14
|
+
}
|
|
15
|
+
function formatTokens(n) {
|
|
16
|
+
if (n < 1e3) return `${n}`;
|
|
17
|
+
if (n < 1e6) return `${(n / 1e3).toFixed(1)}k`;
|
|
18
|
+
return `${(n / 1e6).toFixed(1)}M`;
|
|
19
|
+
}
|
|
20
|
+
function groupThousands(n) {
|
|
21
|
+
const sign = n < 0 ? "-" : "";
|
|
22
|
+
const digits = String(Math.abs(n));
|
|
23
|
+
return sign + digits.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
|
24
|
+
}
|
|
25
|
+
function capitalize(token) {
|
|
26
|
+
if (token.length === 0) return token;
|
|
27
|
+
return token.charAt(0).toUpperCase() + token.slice(1).toLowerCase();
|
|
28
|
+
}
|
|
29
|
+
var ALPHA_ONLY = /^[A-Za-z]+$/;
|
|
30
|
+
var NUMERIC_ONLY = /^\d+$/;
|
|
31
|
+
function modelDisplayName(id) {
|
|
32
|
+
let s = id;
|
|
33
|
+
s = s.replace(/^claude-/, "");
|
|
34
|
+
s = s.replace(/-20\d{6}/, "");
|
|
35
|
+
s = s.replace(/\[1m\]/gi, "");
|
|
36
|
+
const tokens = s.split("-").filter((t) => t.length > 0);
|
|
37
|
+
if (tokens.length === 0) return id;
|
|
38
|
+
if (ALPHA_ONLY.test(tokens[0])) {
|
|
39
|
+
const name = capitalize(tokens[0]);
|
|
40
|
+
const versionTokens = tokens.slice(1).filter((t) => NUMERIC_ONLY.test(t));
|
|
41
|
+
return versionTokens.length > 0 ? `${name} ${versionTokens.join(".")}` : name;
|
|
42
|
+
}
|
|
43
|
+
const alphaTokens = tokens.filter((t) => ALPHA_ONLY.test(t));
|
|
44
|
+
const numericTokens = tokens.filter((t) => NUMERIC_ONLY.test(t));
|
|
45
|
+
if (alphaTokens.length === 1 && numericTokens.length > 0) {
|
|
46
|
+
const name = capitalize(alphaTokens[0]);
|
|
47
|
+
return `${name} ${numericTokens.join(".")}`;
|
|
48
|
+
}
|
|
49
|
+
return id;
|
|
50
|
+
}
|
|
51
|
+
function formatSummary(record, cfg, todayUSD) {
|
|
52
|
+
const label = cfg.costLabel === "api_equivalent" ? "API\u63DB\u7B97 " : "";
|
|
53
|
+
const models = record.models;
|
|
54
|
+
const primaryModel = models[0] ?? "unknown";
|
|
55
|
+
const modelDisp = modelDisplayName(primaryModel) + (models.length > 1 ? ` +${models.length - 1}` : "");
|
|
56
|
+
const title = `\u{1F4B0} ${label}${formatUSD(record.costUSD)}(${formatJPY(record.costJPY)})| ${modelDisp}`;
|
|
57
|
+
const main = record.tokens;
|
|
58
|
+
const side = record.sidechainTokens;
|
|
59
|
+
const effIn = main.input + main.cacheRead + main.cacheWrite5m + main.cacheWrite1h + (side ? side.input + side.cacheRead + side.cacheWrite5m + side.cacheWrite1h : 0);
|
|
60
|
+
const out = main.output + (side ? side.output : 0);
|
|
61
|
+
const cacheTokens = main.cacheRead + main.cacheWrite5m + main.cacheWrite1h + (side ? side.cacheRead + side.cacheWrite5m + side.cacheWrite1h : 0);
|
|
62
|
+
const cachePct = effIn > 0 ? Math.round(cacheTokens / effIn * 100) : 0;
|
|
63
|
+
const projectLabel = path.basename(record.project) || record.project;
|
|
64
|
+
let line1 = `in ${formatTokens(effIn)}(cache ${cachePct}%)/ out ${formatTokens(out)} \xB7 \u{1F4C1} ${projectLabel}`;
|
|
65
|
+
if (cfg.includeDailyTotal && typeof todayUSD === "number") {
|
|
66
|
+
line1 += ` \xB7 \u4ECA\u65E5: ${formatUSD(todayUSD)}`;
|
|
67
|
+
}
|
|
68
|
+
const flattened = (record.prompt ?? "").replace(/\r?\n/g, " ");
|
|
69
|
+
let line2;
|
|
70
|
+
if (flattened.length === 0) {
|
|
71
|
+
line2 = "(\u30D7\u30ED\u30F3\u30D7\u30C8\u306A\u3057)";
|
|
72
|
+
} else if (flattened.length > 50) {
|
|
73
|
+
line2 = `${flattened.slice(0, 50)}\u2026`;
|
|
74
|
+
} else {
|
|
75
|
+
line2 = flattened;
|
|
76
|
+
}
|
|
77
|
+
return { title, body: `${line1}
|
|
78
|
+
${line2}` };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export {
|
|
82
|
+
formatUSD,
|
|
83
|
+
formatJPY,
|
|
84
|
+
formatTokens,
|
|
85
|
+
modelDisplayName,
|
|
86
|
+
formatSummary
|
|
87
|
+
};
|