@nyte-ai/host 0.0.18 → 0.0.19
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/THIRD-PARTY-NOTICES.md +66 -0
- package/dist/chunks/{canvas-preview-5hxat7hh.js → canvas-preview-11nadadc.js} +8 -6
- package/dist/chunks/{canvas-preview-rp6fz0w5.js → canvas-preview-2jwbe14k.js} +5 -5
- package/dist/chunks/{canvas-preview-m02v4yy6.js → canvas-preview-55s0y68g.js} +3 -2
- package/dist/chunks/{canvas-preview-xn10d4f8.js → canvas-preview-5ekjx6sw.js} +9 -4
- package/dist/chunks/{canvas-preview-75db60xy.js → canvas-preview-8xc0vn7v.js} +5 -4
- package/dist/chunks/{canvas-preview-zn2d0k9d.js → canvas-preview-af86jz8t.js} +14 -6
- package/dist/chunks/{canvas-preview-d9wwhbtz.js → canvas-preview-aq5j4p25.js} +1 -1
- package/dist/chunks/{canvas-preview-j1tnf51w.js → canvas-preview-b2v8wfnz.js} +1 -1
- package/dist/chunks/canvas-preview-befkjea2.js +60 -0
- package/dist/chunks/canvas-preview-bxnqt8cd.js +103 -0
- package/dist/chunks/{canvas-preview-vax9ne4e.js → canvas-preview-cawy6vbz.js} +22 -11
- package/dist/chunks/{canvas-preview-senpjwep.js → canvas-preview-crmzxxt8.js} +3 -3
- package/dist/chunks/{canvas-preview-ymgz0937.js → canvas-preview-drctegz4.js} +8 -4
- package/dist/chunks/{canvas-preview-yrfz2ak6.js → canvas-preview-e3vnkkc5.js} +6 -6
- package/dist/chunks/canvas-preview-ec2mt7es.js +108 -0
- package/dist/chunks/{canvas-preview-1ta3avah.js → canvas-preview-fxa0rfb0.js} +10 -5
- package/dist/chunks/{canvas-preview-8xgt67ka.js → canvas-preview-g5fkjs34.js} +1 -1
- package/dist/chunks/{canvas-preview-g3qj3y3k.js → canvas-preview-jeddjex8.js} +13 -3
- package/dist/chunks/{canvas-preview-0nrnbt76.js → canvas-preview-k1yg4fgp.js} +199 -125
- package/dist/chunks/canvas-preview-kp75gb63.js +141 -0
- package/dist/chunks/{canvas-preview-ewgspmsd.js → canvas-preview-n2a35zv5.js} +1 -1
- package/dist/chunks/{canvas-preview-h8hshnff.js → canvas-preview-pkzqg96v.js} +1 -1
- package/dist/chunks/{canvas-preview-evh92ywd.js → canvas-preview-tnqe7k0b.js} +15 -10
- package/dist/chunks/{canvas-preview-ehf3vp31.js → canvas-preview-vm9zdp2t.js} +45 -58
- package/dist/chunks/{canvas-preview-76fwvd6e.js → canvas-preview-w54ymtb3.js} +1 -1
- package/dist/chunks/canvas-preview-y5y6ghaw.js +125 -0
- package/dist/chunks/{canvas-preview-98aj5p34.js → canvas-preview-ycepb05s.js} +1 -1
- package/dist/src/canvas-preview.js +7 -3
- package/dist/src/deploy/cloudflare/worker.js +1 -1
- package/dist/src/deploy/vercel/server/routes/[...].js +2 -2
- package/dist/src/deploy/vercel/server/utils/host.js +1 -1
- package/dist/src/deploy/vercel/server/workflows/drive.js +3 -3
- package/dist/src/images/exif.d.ts +1 -0
- package/dist/src/images/exif.js +6 -0
- package/dist/src/images/node.d.ts +37 -0
- package/dist/src/images/node.js +12 -0
- package/dist/src/images/photon.d.ts +3 -0
- package/dist/src/images/photon.js +7 -0
- package/dist/src/images/worker.d.ts +1 -0
- package/dist/src/images/worker.js +23 -0
- package/dist/src/index.d.ts +0 -1
- package/dist/src/index.js +23 -22
- package/dist/src/listen-fetch.js +3 -3
- package/dist/src/listen.js +4 -4
- package/dist/src/local/command.d.ts +5 -0
- package/dist/src/local/command.js +6 -2
- package/dist/src/local/files.js +2 -2
- package/dist/src/local/git-watch.d.ts +1 -1
- package/dist/src/local/git-watch.js +1 -1
- package/dist/src/local/git.js +3 -3
- package/dist/src/local/github.js +2 -2
- package/dist/src/local/jobs.d.ts +1 -1
- package/dist/src/local/jobs.js +2 -1
- package/dist/src/local/provider.js +9 -9
- package/dist/src/local/snapshots.js +2 -2
- package/dist/src/local/worktrees.js +2 -2
- package/dist/src/mcp/connect.d.ts +0 -13
- package/dist/src/mcp/connect.js +1 -1
- package/dist/src/mcp/hub.js +2 -2
- package/dist/src/open.d.ts +2 -30
- package/dist/src/open.js +20 -18
- package/dist/src/serve/binding.js +1 -1
- package/dist/src/serve/handler.js +2 -2
- package/dist/src/sqlite.d.ts +2 -1
- package/dist/src/sqlite.js +4 -3
- package/dist/src/store-usage.js +4 -1
- package/dist/src/usage-history.d.ts +54 -0
- package/dist/src/usage-history.js +14 -0
- package/dist/src/usage-service.d.ts +2 -1
- package/dist/src/usage-service.js +7 -3
- package/dist/src/usage-worker.js +8 -4
- package/dist/src/usage.d.ts +150 -27
- package/dist/src/usage.js +12 -2
- package/package.json +16 -8
- package/src/canvas-preview.ts +4 -3
- package/src/images/exif.ts +69 -0
- package/src/images/node.ts +163 -0
- package/src/images/photon.ts +141 -0
- package/src/images/worker.ts +29 -0
- package/src/index.ts +19 -1
- package/src/local/command.ts +64 -12
- package/src/local/files.ts +8 -3
- package/src/local/git-watch.ts +16 -6
- package/src/local/git.ts +7 -2
- package/src/local/github.ts +3 -4
- package/src/local/jobs.ts +55 -69
- package/src/local/snapshots.ts +4 -2
- package/src/local/worktrees.ts +4 -4
- package/src/mcp/connect.ts +1 -14
- package/src/open.ts +16 -36
- package/src/serve/binding.ts +4 -3
- package/src/sqlite.ts +33 -8
- package/src/store-usage.ts +5 -2
- package/src/usage-history.ts +219 -0
- package/src/usage-service.ts +12 -3
- package/src/usage.ts +251 -176
- package/dist/chunks/canvas-preview-a3yw0j70.js +0 -72
- package/dist/chunks/canvas-preview-azg56359.js +0 -68
- package/dist/src/store-owner.d.ts +0 -15
- package/dist/src/store-owner.js +0 -9
- package/src/store-owner.ts +0 -98
|
@@ -3,22 +3,61 @@ import {
|
|
|
3
3
|
recordedUsage2,
|
|
4
4
|
summarizeModels2
|
|
5
5
|
} from "./canvas-preview-srgfpdmf.js";
|
|
6
|
+
import {
|
|
7
|
+
localDay2
|
|
8
|
+
} from "./canvas-preview-11nadadc.js";
|
|
9
|
+
import {
|
|
10
|
+
fileScanSchema2,
|
|
11
|
+
UsageScanCache2,
|
|
12
|
+
readHistory2,
|
|
13
|
+
readCacheDocument2,
|
|
14
|
+
writeCacheDocument2
|
|
15
|
+
} from "./canvas-preview-kp75gb63.js";
|
|
6
16
|
|
|
7
17
|
// src/usage.ts
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import { lstat, open, opendir, stat } from "node:fs/promises";
|
|
18
|
+
import { existsSync } from "node:fs";
|
|
19
|
+
import { lstat, opendir, stat } from "node:fs/promises";
|
|
11
20
|
import { homedir } from "node:os";
|
|
12
21
|
import { join, resolve } from "node:path";
|
|
13
22
|
import process from "node:process";
|
|
14
|
-
import { StringDecoder } from "node:string_decoder";
|
|
15
23
|
import { Type } from "typebox";
|
|
24
|
+
import { Compile } from "typebox/compile";
|
|
16
25
|
import { Value } from "typebox/value";
|
|
17
26
|
import { calculateCost } from "@nyte-ai/ai";
|
|
18
27
|
var tokens = Type.Integer({ minimum: 0, maximum: Number.MAX_SAFE_INTEGER });
|
|
28
|
+
var count = Type.Number({ minimum: 0 });
|
|
29
|
+
var modelUsage = Type.Object({
|
|
30
|
+
input: count,
|
|
31
|
+
output: count,
|
|
32
|
+
cacheRead: count,
|
|
33
|
+
cacheWrite: count,
|
|
34
|
+
cacheWrite1h: Type.Optional(count),
|
|
35
|
+
reasoning: Type.Optional(count),
|
|
36
|
+
totalTokens: count,
|
|
37
|
+
cost: Type.Object({
|
|
38
|
+
input: Type.Number(),
|
|
39
|
+
output: Type.Number(),
|
|
40
|
+
cacheRead: Type.Number(),
|
|
41
|
+
cacheWrite: Type.Number(),
|
|
42
|
+
total: Type.Number()
|
|
43
|
+
})
|
|
44
|
+
});
|
|
45
|
+
var recordedAt = Type.Union([Type.Number(), Type.Null()]);
|
|
46
|
+
var snapshot = Type.Object({
|
|
47
|
+
model: Type.String(),
|
|
48
|
+
usage: modelUsage,
|
|
49
|
+
costUSD: Type.Optional(Type.Number()),
|
|
50
|
+
at: recordedAt
|
|
51
|
+
});
|
|
52
|
+
var claudeCodeFileScan = Type.Object({
|
|
53
|
+
keyed: Type.Array(Type.Tuple([Type.String(), snapshot])),
|
|
54
|
+
anonymous: Type.Array(snapshot),
|
|
55
|
+
malformedRecords: Type.Integer({ minimum: 0 })
|
|
56
|
+
});
|
|
19
57
|
var envelope = Type.Object({ type: Type.String() });
|
|
20
58
|
var assistantRecord = Type.Object({
|
|
21
59
|
type: Type.Literal("assistant"),
|
|
60
|
+
timestamp: Type.Optional(Type.String()),
|
|
22
61
|
requestId: Type.Optional(Type.String()),
|
|
23
62
|
costUSD: Type.Optional(Type.Unknown()),
|
|
24
63
|
message: Type.Object({
|
|
@@ -43,38 +82,49 @@ function betterSnapshot(previous, next) {
|
|
|
43
82
|
const usage = next.usage;
|
|
44
83
|
return usage.totalTokens > previous.usage.totalTokens || usage.totalTokens === previous.usage.totalTokens && (usage.output > previous.usage.output || usage.output === previous.usage.output && previous.costUSD === undefined && next.costUSD !== undefined);
|
|
45
84
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
85
|
+
function parseRecordedAt(value) {
|
|
86
|
+
if (value === undefined)
|
|
87
|
+
return null;
|
|
88
|
+
const parsed = Date.parse(value);
|
|
89
|
+
return Number.isNaN(parsed) ? null : parsed;
|
|
90
|
+
}
|
|
91
|
+
function recordedIn(window, at) {
|
|
92
|
+
if (at === null)
|
|
93
|
+
return window.sinceDay === null;
|
|
94
|
+
const day = localDay2(at);
|
|
95
|
+
return day <= window.untilDay && (window.sinceDay === null || day >= window.sinceDay);
|
|
96
|
+
}
|
|
97
|
+
var claudeCodeParser = (from) => {
|
|
98
|
+
const keyed = new Map(from?.keyed);
|
|
99
|
+
const anonymous = [...from?.anonymous ?? []];
|
|
100
|
+
let malformedRecords = from?.malformedRecords ?? 0;
|
|
101
|
+
const line = ({ text, terminated }) => {
|
|
102
|
+
if (!text.trim())
|
|
103
|
+
return;
|
|
54
104
|
let value;
|
|
55
105
|
try {
|
|
56
|
-
value = JSON.parse(
|
|
106
|
+
value = JSON.parse(text);
|
|
57
107
|
} catch {
|
|
58
|
-
if (
|
|
108
|
+
if (terminated)
|
|
59
109
|
malformedRecords++;
|
|
60
|
-
|
|
110
|
+
return;
|
|
61
111
|
}
|
|
62
112
|
if (!Value.Check(envelope, value)) {
|
|
63
113
|
malformedRecords++;
|
|
64
|
-
|
|
114
|
+
return;
|
|
65
115
|
}
|
|
66
116
|
if (value.type !== "assistant")
|
|
67
|
-
|
|
117
|
+
return;
|
|
68
118
|
if (!Value.Check(assistantRecord, value)) {
|
|
69
119
|
malformedRecords++;
|
|
70
|
-
|
|
120
|
+
return;
|
|
71
121
|
}
|
|
72
122
|
const raw = value.message.usage;
|
|
73
123
|
const cacheWrite = raw.cache_creation_input_tokens ?? 0;
|
|
74
124
|
const cacheWrite1h = raw.cache_creation?.ephemeral_1h_input_tokens;
|
|
75
125
|
if ((cacheWrite1h ?? 0) + (raw.cache_creation?.ephemeral_5m_input_tokens ?? 0) > cacheWrite) {
|
|
76
126
|
malformedRecords++;
|
|
77
|
-
|
|
127
|
+
return;
|
|
78
128
|
}
|
|
79
129
|
const usage = {
|
|
80
130
|
...emptyModelUsage2(),
|
|
@@ -89,40 +139,31 @@ async function scanClaudeCodeFile(path, signal) {
|
|
|
89
139
|
const snapshot = {
|
|
90
140
|
model: value.message.model,
|
|
91
141
|
usage,
|
|
92
|
-
|
|
142
|
+
at: parseRecordedAt(value.timestamp)
|
|
93
143
|
};
|
|
144
|
+
if (Value.Check(reportedCost, value.costUSD))
|
|
145
|
+
snapshot.costUSD = value.costUSD;
|
|
94
146
|
const messageId = value.message.id?.trim() || null;
|
|
95
147
|
const requestId = value.requestId?.trim() || null;
|
|
96
148
|
if (messageId === null && requestId === null) {
|
|
97
149
|
anonymous.push(snapshot);
|
|
98
|
-
|
|
150
|
+
return;
|
|
99
151
|
}
|
|
100
152
|
const identity = JSON.stringify([messageId, requestId]);
|
|
101
153
|
if (betterSnapshot(keyed.get(identity), snapshot))
|
|
102
154
|
keyed.set(identity, snapshot);
|
|
103
|
-
}
|
|
104
|
-
return { keyed: [...keyed], anonymous, malformedRecords };
|
|
105
|
-
}
|
|
106
|
-
async function
|
|
107
|
-
seen.add(path);
|
|
108
|
-
if (cache === undefined)
|
|
109
|
-
return parse();
|
|
155
|
+
};
|
|
156
|
+
return { line, scan: () => ({ keyed: [...keyed], anonymous: [...anonymous], malformedRecords }) };
|
|
157
|
+
};
|
|
158
|
+
async function readHistoryInWindow(path, window, read) {
|
|
159
|
+
read.seen.add(path);
|
|
110
160
|
const info = await lstat(path);
|
|
111
161
|
if (!info.isFile())
|
|
112
162
|
throw new Error("History is not a regular file.");
|
|
113
|
-
const
|
|
114
|
-
if (
|
|
115
|
-
return hit.scan;
|
|
116
|
-
const scan = await parse();
|
|
117
|
-
cache.set(path, { size: info.size, mtimeMs: info.mtimeMs, scan });
|
|
118
|
-
return scan;
|
|
119
|
-
}
|
|
120
|
-
function pruneCache(cache, seen) {
|
|
121
|
-
if (cache === undefined)
|
|
163
|
+
const opens = window.sinceDay === null ? 0 : new Date(`${window.sinceDay}T00:00:00`).getTime();
|
|
164
|
+
if (info.mtimeMs < opens)
|
|
122
165
|
return;
|
|
123
|
-
|
|
124
|
-
if (!seen.has(path))
|
|
125
|
-
cache.delete(path);
|
|
166
|
+
return readHistory2(path, info, read.parser, read.cache, read.signal);
|
|
126
167
|
}
|
|
127
168
|
async function readClaudeCodeUsage2(options) {
|
|
128
169
|
options.signal?.throwIfAborted();
|
|
@@ -161,7 +202,14 @@ async function readClaudeCodeUsage2(options) {
|
|
|
161
202
|
if (!entry.isFile() || !entry.name.endsWith(".jsonl"))
|
|
162
203
|
continue;
|
|
163
204
|
try {
|
|
164
|
-
const scan = await
|
|
205
|
+
const scan = await readHistoryInWindow(path, options.window, {
|
|
206
|
+
parser: claudeCodeParser,
|
|
207
|
+
cache: options.cache,
|
|
208
|
+
seen,
|
|
209
|
+
signal: options.signal
|
|
210
|
+
});
|
|
211
|
+
if (scan === undefined)
|
|
212
|
+
continue;
|
|
165
213
|
malformedRecords += scan.malformedRecords;
|
|
166
214
|
anonymous.push(...scan.anonymous);
|
|
167
215
|
for (const [identity, snapshot] of scan.keyed) {
|
|
@@ -181,12 +229,14 @@ async function readClaudeCodeUsage2(options) {
|
|
|
181
229
|
}
|
|
182
230
|
}
|
|
183
231
|
options.signal?.throwIfAborted();
|
|
184
|
-
|
|
232
|
+
options.cache?.keepOnly(seen);
|
|
185
233
|
const catalog = options.models.getModels("anthropic").filter((model) => model.provider === "anthropic");
|
|
186
234
|
const rows = [];
|
|
187
235
|
let unpricedRecords = 0;
|
|
188
236
|
for (const snapshot of [...keyed.values(), ...anonymous]) {
|
|
189
237
|
options.signal?.throwIfAborted();
|
|
238
|
+
if (!recordedIn(options.window, snapshot.at))
|
|
239
|
+
continue;
|
|
190
240
|
const usage = { ...snapshot.usage, cost: { ...snapshot.usage.cost } };
|
|
191
241
|
if (snapshot.costUSD !== undefined) {
|
|
192
242
|
usage.cost.total = snapshot.costUSD;
|
|
@@ -218,6 +268,19 @@ async function readClaudeCodeUsage2(options) {
|
|
|
218
268
|
return { kind: "failed", message: "Could not read Claude Code local history." };
|
|
219
269
|
}
|
|
220
270
|
}
|
|
271
|
+
var codexRow = Type.Object({ model: Type.String(), usage: modelUsage, at: recordedAt });
|
|
272
|
+
var codexCursor = Type.Object({
|
|
273
|
+
model: Type.String(),
|
|
274
|
+
sawMeta: Type.Boolean(),
|
|
275
|
+
signature: Type.Optional(Type.String()),
|
|
276
|
+
forkAnchorMs: Type.Optional(Type.Number())
|
|
277
|
+
});
|
|
278
|
+
var codexFileScan = Type.Object({
|
|
279
|
+
modern: Type.Array(Type.Tuple([Type.String(), codexRow])),
|
|
280
|
+
legacy: Type.Array(codexRow),
|
|
281
|
+
malformedRecords: Type.Integer({ minimum: 0 }),
|
|
282
|
+
cursor: codexCursor
|
|
283
|
+
});
|
|
221
284
|
var codexTokens = Type.Object({
|
|
222
285
|
input_tokens: Type.Optional(tokens),
|
|
223
286
|
cached_input_tokens: Type.Optional(tokens),
|
|
@@ -227,6 +290,7 @@ var codexTokens = Type.Object({
|
|
|
227
290
|
});
|
|
228
291
|
var usageRecord = Type.Object({
|
|
229
292
|
type: Type.Literal("token_usage_record"),
|
|
293
|
+
timestamp: Type.Optional(Type.String()),
|
|
230
294
|
payload: Type.Object({
|
|
231
295
|
response_id: Type.Optional(Type.String()),
|
|
232
296
|
usage: codexTokens
|
|
@@ -269,12 +333,6 @@ function codexUsage(raw) {
|
|
|
269
333
|
};
|
|
270
334
|
}
|
|
271
335
|
var FORK_COPY_MAX_GAP_MS = 1000;
|
|
272
|
-
function parseMs(value) {
|
|
273
|
-
if (value === undefined)
|
|
274
|
-
return;
|
|
275
|
-
const parsed = Date.parse(value);
|
|
276
|
-
return Number.isNaN(parsed) ? undefined : parsed;
|
|
277
|
-
}
|
|
278
336
|
async function readCodexUsage2(options) {
|
|
279
337
|
options.signal?.throwIfAborted();
|
|
280
338
|
const home = resolve(options.homeDir ?? (process.env.CODEX_HOME?.trim() || join(homedir(), ".codex")));
|
|
@@ -302,7 +360,14 @@ async function readCodexUsage2(options) {
|
|
|
302
360
|
for (const root of present) {
|
|
303
361
|
for await (const path of jsonlFiles(root, options.signal)) {
|
|
304
362
|
try {
|
|
305
|
-
const scan = await
|
|
363
|
+
const scan = await readHistoryInWindow(path, options.window, {
|
|
364
|
+
parser: codexParser,
|
|
365
|
+
cache: options.cache,
|
|
366
|
+
seen,
|
|
367
|
+
signal: options.signal
|
|
368
|
+
});
|
|
369
|
+
if (scan === undefined)
|
|
370
|
+
continue;
|
|
306
371
|
malformedRecords += scan.malformedRecords;
|
|
307
372
|
if (scan.modern.length > 0) {
|
|
308
373
|
for (const [id, row] of scan.modern)
|
|
@@ -318,12 +383,14 @@ async function readCodexUsage2(options) {
|
|
|
318
383
|
}
|
|
319
384
|
}
|
|
320
385
|
options.signal?.throwIfAborted();
|
|
321
|
-
|
|
386
|
+
options.cache?.keepOnly(seen);
|
|
322
387
|
const catalog = options.models.getModels("openai-codex").filter((model) => model.provider === "openai-codex");
|
|
323
388
|
const rows = [];
|
|
324
389
|
let unpricedRecords = 0;
|
|
325
390
|
for (const row of [...responses.values(), ...legacy]) {
|
|
326
391
|
options.signal?.throwIfAborted();
|
|
392
|
+
if (!recordedIn(options.window, row.at))
|
|
393
|
+
continue;
|
|
327
394
|
const usage = { ...row.usage, cost: { ...row.usage.cost } };
|
|
328
395
|
const matches = catalog.filter((model) => model.id === row.model);
|
|
329
396
|
const model = matches.length === 1 ? matches[0] : undefined;
|
|
@@ -350,76 +417,81 @@ async function readCodexUsage2(options) {
|
|
|
350
417
|
return { kind: "failed", message: "Could not read Codex local history." };
|
|
351
418
|
}
|
|
352
419
|
}
|
|
353
|
-
|
|
354
|
-
const modern = new Map;
|
|
355
|
-
const legacy = [];
|
|
356
|
-
let malformedRecords = 0;
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
for await (const line of historyLines(path, signal)) {
|
|
362
|
-
signal?.throwIfAborted();
|
|
363
|
-
if (!line.text.trim())
|
|
364
|
-
continue;
|
|
420
|
+
var codexParser = (from) => {
|
|
421
|
+
const modern = new Map(from?.modern);
|
|
422
|
+
const legacy = [...from?.legacy ?? []];
|
|
423
|
+
let malformedRecords = from?.malformedRecords ?? 0;
|
|
424
|
+
const cursor = { ...from?.cursor ?? { model: "", sawMeta: false } };
|
|
425
|
+
const line = ({ text, terminated }) => {
|
|
426
|
+
if (!text.trim())
|
|
427
|
+
return;
|
|
365
428
|
let value;
|
|
366
429
|
try {
|
|
367
|
-
value = JSON.parse(
|
|
430
|
+
value = JSON.parse(text);
|
|
368
431
|
} catch {
|
|
369
|
-
if (
|
|
432
|
+
if (terminated)
|
|
370
433
|
malformedRecords++;
|
|
371
|
-
|
|
434
|
+
return;
|
|
372
435
|
}
|
|
373
436
|
if (!Value.Check(envelope, value)) {
|
|
374
437
|
malformedRecords++;
|
|
375
|
-
|
|
438
|
+
return;
|
|
376
439
|
}
|
|
377
440
|
if (Value.Check(turnContext, value)) {
|
|
378
441
|
if (value.payload.model !== undefined)
|
|
379
|
-
model = value.payload.model;
|
|
380
|
-
|
|
442
|
+
cursor.model = value.payload.model;
|
|
443
|
+
return;
|
|
381
444
|
}
|
|
382
445
|
if (Value.Check(sessionMeta, value)) {
|
|
383
|
-
if (sawMeta)
|
|
384
|
-
|
|
385
|
-
sawMeta = true;
|
|
446
|
+
if (cursor.sawMeta)
|
|
447
|
+
return;
|
|
448
|
+
cursor.sawMeta = true;
|
|
386
449
|
const forked = value.payload.forked_from_id !== undefined || value.payload.parent_thread_id !== undefined;
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
450
|
+
const anchor = parseRecordedAt(value.timestamp);
|
|
451
|
+
if (forked && anchor !== null)
|
|
452
|
+
cursor.forkAnchorMs = anchor;
|
|
453
|
+
return;
|
|
390
454
|
}
|
|
391
455
|
if (Value.Check(usageRecord, value)) {
|
|
392
456
|
const id = value.payload.response_id;
|
|
393
|
-
if (id === undefined || model === "")
|
|
394
|
-
|
|
457
|
+
if (id === undefined || cursor.model === "")
|
|
458
|
+
return;
|
|
395
459
|
const usage = codexUsage(value.payload.usage);
|
|
396
460
|
if (usage.totalTokens > 0 && !modern.has(id))
|
|
397
|
-
modern.set(id, { model, usage });
|
|
398
|
-
|
|
461
|
+
modern.set(id, { model: cursor.model, usage, at: parseRecordedAt(value.timestamp) });
|
|
462
|
+
return;
|
|
399
463
|
}
|
|
400
464
|
if (!Value.Check(tokenCount, value))
|
|
401
|
-
|
|
465
|
+
return;
|
|
402
466
|
const last = value.payload.info.last_token_usage;
|
|
403
|
-
if (last === undefined || model === "")
|
|
404
|
-
|
|
467
|
+
if (last === undefined || cursor.model === "")
|
|
468
|
+
return;
|
|
405
469
|
const next = JSON.stringify(last);
|
|
406
|
-
if (next === signature)
|
|
407
|
-
|
|
408
|
-
signature = next;
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
if (at !==
|
|
412
|
-
forkAnchorMs = at;
|
|
413
|
-
|
|
470
|
+
if (next === cursor.signature)
|
|
471
|
+
return;
|
|
472
|
+
cursor.signature = next;
|
|
473
|
+
const at = parseRecordedAt(value.timestamp);
|
|
474
|
+
if (cursor.forkAnchorMs !== undefined) {
|
|
475
|
+
if (at !== null && at - cursor.forkAnchorMs < FORK_COPY_MAX_GAP_MS) {
|
|
476
|
+
cursor.forkAnchorMs = at;
|
|
477
|
+
return;
|
|
414
478
|
}
|
|
415
|
-
forkAnchorMs
|
|
479
|
+
delete cursor.forkAnchorMs;
|
|
416
480
|
}
|
|
417
481
|
const usage = codexUsage(last);
|
|
418
482
|
if (usage.totalTokens > 0)
|
|
419
|
-
legacy.push({ model, usage });
|
|
420
|
-
}
|
|
421
|
-
return {
|
|
422
|
-
|
|
483
|
+
legacy.push({ model: cursor.model, usage, at });
|
|
484
|
+
};
|
|
485
|
+
return {
|
|
486
|
+
line,
|
|
487
|
+
scan: () => ({
|
|
488
|
+
modern: [...modern],
|
|
489
|
+
legacy: [...legacy],
|
|
490
|
+
malformedRecords,
|
|
491
|
+
cursor: { ...cursor }
|
|
492
|
+
})
|
|
493
|
+
};
|
|
494
|
+
};
|
|
423
495
|
async function* jsonlFiles(root, signal) {
|
|
424
496
|
const directories = [root];
|
|
425
497
|
for (const directory of directories) {
|
|
@@ -441,37 +513,37 @@ async function* jsonlFiles(root, signal) {
|
|
|
441
513
|
}
|
|
442
514
|
}
|
|
443
515
|
}
|
|
444
|
-
async function* historyLines(path, signal) {
|
|
445
|
-
const file = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
446
|
-
try {
|
|
447
|
-
const stat = await file.stat();
|
|
448
|
-
if (!stat.isFile())
|
|
449
|
-
throw new Error("History is not a regular file.");
|
|
450
|
-
const buffer = Buffer.alloc(64 * 1024);
|
|
451
|
-
const decoder = new StringDecoder("utf8");
|
|
452
|
-
let pending = "";
|
|
453
|
-
let position = 0;
|
|
454
|
-
while (position < stat.size) {
|
|
455
|
-
signal?.throwIfAborted();
|
|
456
|
-
const { bytesRead } = await file.read(buffer, 0, Math.min(buffer.length, stat.size - position), position);
|
|
457
|
-
if (bytesRead === 0)
|
|
458
|
-
break;
|
|
459
|
-
position += bytesRead;
|
|
460
|
-
const lines = `${pending}${decoder.write(buffer.subarray(0, bytesRead))}`.split(`
|
|
461
|
-
`);
|
|
462
|
-
pending = lines.pop() ?? "";
|
|
463
|
-
for (const text of lines)
|
|
464
|
-
yield { text, terminated: true };
|
|
465
|
-
}
|
|
466
|
-
pending += decoder.end();
|
|
467
|
-
if (pending)
|
|
468
|
-
yield { text: pending, terminated: false };
|
|
469
|
-
} finally {
|
|
470
|
-
await file.close();
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
516
|
function createUsageScanCaches2() {
|
|
474
|
-
return { claudeCode: new
|
|
517
|
+
return { claudeCode: new UsageScanCache2, codex: new UsageScanCache2 };
|
|
518
|
+
}
|
|
519
|
+
var USAGE_SCAN_CACHE_VERSION = 1;
|
|
520
|
+
var usageScanCacheDocument = Type.Object({
|
|
521
|
+
version: Type.Literal(USAGE_SCAN_CACHE_VERSION),
|
|
522
|
+
claudeCode: Type.Array(Type.Tuple([Type.String(), fileScanSchema2(claudeCodeFileScan)])),
|
|
523
|
+
codex: Type.Array(Type.Tuple([Type.String(), fileScanSchema2(codexFileScan)]))
|
|
524
|
+
});
|
|
525
|
+
var usageScanCacheValidator = Compile(usageScanCacheDocument);
|
|
526
|
+
async function loadUsageScanCaches2(path) {
|
|
527
|
+
const document = await readCacheDocument2(path, usageScanCacheValidator);
|
|
528
|
+
if (document === undefined)
|
|
529
|
+
return createUsageScanCaches2();
|
|
530
|
+
return {
|
|
531
|
+
claudeCode: new UsageScanCache2(document.claudeCode),
|
|
532
|
+
codex: new UsageScanCache2(document.codex)
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
async function saveUsageScanCaches2(path, caches) {
|
|
536
|
+
if (!caches.claudeCode.changed && !caches.codex.changed)
|
|
537
|
+
return;
|
|
538
|
+
const saved = await writeCacheDocument2(path, {
|
|
539
|
+
version: USAGE_SCAN_CACHE_VERSION,
|
|
540
|
+
claudeCode: [...caches.claudeCode.entries()],
|
|
541
|
+
codex: [...caches.codex.entries()]
|
|
542
|
+
});
|
|
543
|
+
if (!saved)
|
|
544
|
+
return;
|
|
545
|
+
caches.claudeCode.markSaved();
|
|
546
|
+
caches.codex.markSaved();
|
|
475
547
|
}
|
|
476
548
|
async function readLocalUsage2(options) {
|
|
477
549
|
const failed = (message) => {
|
|
@@ -481,11 +553,13 @@ async function readLocalUsage2(options) {
|
|
|
481
553
|
const [claudeCode, codex] = await Promise.all([
|
|
482
554
|
readClaudeCodeUsage2({
|
|
483
555
|
models: options.models,
|
|
556
|
+
window: options.window,
|
|
484
557
|
signal: options.signal,
|
|
485
558
|
cache: options.caches?.claudeCode
|
|
486
559
|
}).catch(() => failed("Could not read Claude Code history.")),
|
|
487
560
|
readCodexUsage2({
|
|
488
561
|
models: options.models,
|
|
562
|
+
window: options.window,
|
|
489
563
|
signal: options.signal,
|
|
490
564
|
cache: options.caches?.codex
|
|
491
565
|
}).catch(() => failed("Could not read Codex history."))
|
|
@@ -494,4 +568,4 @@ async function readLocalUsage2(options) {
|
|
|
494
568
|
return { claudeCode, codex };
|
|
495
569
|
}
|
|
496
570
|
|
|
497
|
-
export { readClaudeCodeUsage2, readCodexUsage2, createUsageScanCaches2, readLocalUsage2 };
|
|
571
|
+
export { readClaudeCodeUsage2, readCodexUsage2, createUsageScanCaches2, loadUsageScanCaches2, saveUsageScanCaches2, readLocalUsage2 };
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// src/usage-history.ts
|
|
2
|
+
import { Buffer } from "node:buffer";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { constants } from "node:fs";
|
|
5
|
+
import { mkdir, open, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
6
|
+
import { dirname } from "node:path";
|
|
7
|
+
import { Type } from "typebox";
|
|
8
|
+
function fileScanSchema2(scan) {
|
|
9
|
+
return Type.Object({
|
|
10
|
+
size: Type.Number({ minimum: 0 }),
|
|
11
|
+
mtimeMs: Type.Number(),
|
|
12
|
+
position: Type.Number({ minimum: 0 }),
|
|
13
|
+
committed: scan
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
class UsageScanCache2 {
|
|
18
|
+
#files;
|
|
19
|
+
#changed = false;
|
|
20
|
+
constructor(files = []) {
|
|
21
|
+
this.#files = new Map(files);
|
|
22
|
+
}
|
|
23
|
+
keys() {
|
|
24
|
+
return this.#files.keys();
|
|
25
|
+
}
|
|
26
|
+
entries() {
|
|
27
|
+
return this.#files.entries();
|
|
28
|
+
}
|
|
29
|
+
get(path) {
|
|
30
|
+
return this.#files.get(path);
|
|
31
|
+
}
|
|
32
|
+
set(path, scan) {
|
|
33
|
+
if (this.#files.get(path) === scan)
|
|
34
|
+
return;
|
|
35
|
+
this.#files.set(path, scan);
|
|
36
|
+
this.#changed = true;
|
|
37
|
+
}
|
|
38
|
+
keepOnly(seen) {
|
|
39
|
+
for (const path of this.#files.keys()) {
|
|
40
|
+
if (seen.has(path))
|
|
41
|
+
continue;
|
|
42
|
+
this.#files.delete(path);
|
|
43
|
+
this.#changed = true;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
get changed() {
|
|
47
|
+
return this.#changed;
|
|
48
|
+
}
|
|
49
|
+
markSaved() {
|
|
50
|
+
this.#changed = false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
async function readHistory2(path, info, parser, cache, signal) {
|
|
54
|
+
const hit = cache?.get(path);
|
|
55
|
+
const unchanged = hit !== undefined && hit.size === info.size && hit.mtimeMs === info.mtimeMs;
|
|
56
|
+
if (unchanged && hit.position === info.size)
|
|
57
|
+
return hit.committed;
|
|
58
|
+
const from = unchanged || hit !== undefined && info.size > hit.size ? hit : undefined;
|
|
59
|
+
const fold = parser(from?.committed);
|
|
60
|
+
let position = from?.position ?? 0;
|
|
61
|
+
let unterminated;
|
|
62
|
+
for await (const line of historyLines(path, position, signal)) {
|
|
63
|
+
signal?.throwIfAborted();
|
|
64
|
+
if (!line.terminated) {
|
|
65
|
+
unterminated = line;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
fold.line(line);
|
|
69
|
+
position = line.end;
|
|
70
|
+
}
|
|
71
|
+
const committed = fold.scan();
|
|
72
|
+
cache?.set(path, unchanged ? hit : { size: info.size, mtimeMs: info.mtimeMs, position, committed });
|
|
73
|
+
if (unterminated === undefined)
|
|
74
|
+
return committed;
|
|
75
|
+
fold.line(unterminated);
|
|
76
|
+
return fold.scan();
|
|
77
|
+
}
|
|
78
|
+
async function* historyLines(path, start, signal) {
|
|
79
|
+
const file = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
80
|
+
try {
|
|
81
|
+
const stat = await file.stat();
|
|
82
|
+
if (!stat.isFile())
|
|
83
|
+
throw new Error("History is not a regular file.");
|
|
84
|
+
const buffer = Buffer.alloc(64 * 1024);
|
|
85
|
+
let pending = Buffer.alloc(0);
|
|
86
|
+
let pendingStart = start;
|
|
87
|
+
let position = start;
|
|
88
|
+
while (position < stat.size) {
|
|
89
|
+
signal?.throwIfAborted();
|
|
90
|
+
const { bytesRead } = await file.read(buffer, 0, Math.min(buffer.length, stat.size - position), position);
|
|
91
|
+
if (bytesRead === 0)
|
|
92
|
+
break;
|
|
93
|
+
position += bytesRead;
|
|
94
|
+
const read = buffer.subarray(0, bytesRead);
|
|
95
|
+
const chunk = pending.length === 0 ? read : Buffer.concat([pending, read]);
|
|
96
|
+
let from = 0;
|
|
97
|
+
for (let newline = chunk.indexOf(10);newline !== -1; newline = chunk.indexOf(10, from)) {
|
|
98
|
+
yield {
|
|
99
|
+
text: chunk.toString("utf8", from, newline),
|
|
100
|
+
terminated: true,
|
|
101
|
+
end: pendingStart + newline + 1
|
|
102
|
+
};
|
|
103
|
+
from = newline + 1;
|
|
104
|
+
}
|
|
105
|
+
pendingStart += from;
|
|
106
|
+
pending = Buffer.from(chunk.subarray(from));
|
|
107
|
+
}
|
|
108
|
+
if (pending.length > 0)
|
|
109
|
+
yield {
|
|
110
|
+
text: pending.toString("utf8"),
|
|
111
|
+
terminated: false,
|
|
112
|
+
end: pendingStart + pending.length
|
|
113
|
+
};
|
|
114
|
+
} finally {
|
|
115
|
+
await file.close();
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
async function readCacheDocument2(path, validator) {
|
|
119
|
+
try {
|
|
120
|
+
const value = JSON.parse(await readFile(path, "utf8"));
|
|
121
|
+
return validator.Check(value) ? value : undefined;
|
|
122
|
+
} catch {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
async function writeCacheDocument2(path, document) {
|
|
127
|
+
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
128
|
+
try {
|
|
129
|
+
await mkdir(dirname(path), { recursive: true });
|
|
130
|
+
await writeFile(temporary, JSON.stringify(document), { mode: 384 });
|
|
131
|
+
await rename(temporary, path);
|
|
132
|
+
return true;
|
|
133
|
+
} catch {
|
|
134
|
+
await rm(temporary, { force: true }).catch(() => {
|
|
135
|
+
return;
|
|
136
|
+
});
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export { fileScanSchema2, UsageScanCache2, readHistory2, readCacheDocument2, writeCacheDocument2 };
|
|
@@ -54,7 +54,7 @@ function expand(values, server) {
|
|
|
54
54
|
value.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (_match, name) => {
|
|
55
55
|
const found = process.env[name];
|
|
56
56
|
if (found === undefined)
|
|
57
|
-
throw new Error(`${server} needs the environment variable ${name}. Set it where Nyte runs, then restart Nyte
|
|
57
|
+
throw new Error(`${server} needs the environment variable ${name}. Set it where Nyte runs, then restart Nyte.`);
|
|
58
58
|
return found;
|
|
59
59
|
})
|
|
60
60
|
]));
|