opencode-token-tracker 1.5.2 → 1.5.4
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/dist/bin/opencode-tokens.js +51 -14
- package/dist/index.js +29 -1
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { BUILTIN_PRICING, DEFAULT_CONFIG, findModelConfigPricing, formatCost, formatTokens, getStartOfDay, getStartOfWeek, getStartOfMonth, validateConfig } from "../lib/shared.js";
|
|
3
|
-
import { readFileSync, existsSync, writeFileSync } from "fs";
|
|
3
|
+
import { readFileSync, existsSync, writeFileSync, openSync, readSync, closeSync, statSync } from "fs";
|
|
4
4
|
import { join } from "path";
|
|
5
5
|
import { homedir } from "os";
|
|
6
6
|
const CONFIG_DIR = join(homedir(), ".config", "opencode");
|
|
@@ -22,25 +22,62 @@ function loadEntries(since) {
|
|
|
22
22
|
if (!existsSync(LOG_FILE)) {
|
|
23
23
|
return [];
|
|
24
24
|
}
|
|
25
|
-
const content = readFileSync(LOG_FILE, "utf-8");
|
|
26
|
-
const lines = content.trim().split("\n").filter(Boolean);
|
|
27
25
|
const entries = [];
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
26
|
+
const fd = openSync(LOG_FILE, "r");
|
|
27
|
+
const stat = statSync(LOG_FILE);
|
|
28
|
+
const fileSize = stat.size;
|
|
29
|
+
const CHUNK_SIZE = 64 * 1024; // 64KB chunks
|
|
30
|
+
const buffer = Buffer.alloc(CHUNK_SIZE);
|
|
31
|
+
let filePos = fileSize;
|
|
32
|
+
let leftover = "";
|
|
33
|
+
let shouldStop = false;
|
|
34
|
+
while (filePos > 0 && !shouldStop) {
|
|
35
|
+
const readLength = Math.min(CHUNK_SIZE, filePos);
|
|
36
|
+
filePos -= readLength;
|
|
37
|
+
readSync(fd, buffer, 0, readLength, filePos);
|
|
38
|
+
const chunkStr = buffer.toString("utf8", 0, readLength) + leftover;
|
|
39
|
+
const lines = chunkStr.split("\n");
|
|
40
|
+
// The leftmost line could be cut off, save it for the next chunk read to the left
|
|
41
|
+
leftover = lines[0];
|
|
42
|
+
// Iterate lines in reverse order (from end to start)
|
|
43
|
+
for (let i = lines.length - 1; i >= 1; i--) {
|
|
44
|
+
const line = lines[i].trim();
|
|
45
|
+
if (!line)
|
|
36
46
|
continue;
|
|
37
|
-
|
|
47
|
+
try {
|
|
48
|
+
const entry = JSON.parse(line);
|
|
49
|
+
if (entry.type !== "tokens")
|
|
50
|
+
continue;
|
|
51
|
+
// Early break pruning: once we hit a record older than the threshold,
|
|
52
|
+
// we can safely stop reading earlier history thanks to monotonic time progression in JSONL.
|
|
53
|
+
if (since && entry._ts < since) {
|
|
54
|
+
shouldStop = true;
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
if (!entry.input && !entry.output)
|
|
58
|
+
continue;
|
|
59
|
+
entries.push(entry);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// Skip malformed lines
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// Include the very first line at the top
|
|
67
|
+
if (!shouldStop && leftover.trim()) {
|
|
68
|
+
try {
|
|
69
|
+
const entry = JSON.parse(leftover.trim());
|
|
70
|
+
if (entry.type === "tokens" && (!since || entry._ts >= since) && (entry.input || entry.output)) {
|
|
71
|
+
entries.push(entry);
|
|
72
|
+
}
|
|
38
73
|
}
|
|
39
74
|
catch {
|
|
40
|
-
//
|
|
75
|
+
// Ignore
|
|
41
76
|
}
|
|
42
77
|
}
|
|
43
|
-
|
|
78
|
+
closeSync(fd);
|
|
79
|
+
// Re-establish original chronological order
|
|
80
|
+
return entries.reverse();
|
|
44
81
|
}
|
|
45
82
|
function loadConfig() {
|
|
46
83
|
try {
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { BUILTIN_PRICING, DEFAULT_CONFIG, findModelConfigPricing, formatCost, formatTokens, getStartOfDay, getStartOfWeek, getStartOfMonth, validateConfig } from "./lib/shared.js";
|
|
2
|
-
import { appendFileSync, existsSync, mkdirSync, readFileSync } from "fs";
|
|
2
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, statSync } from "fs";
|
|
3
3
|
import { join } from "path";
|
|
4
4
|
import { homedir } from "os";
|
|
5
5
|
const CONFIG_DIR = join(homedir(), ".config", "opencode");
|
|
@@ -11,6 +11,8 @@ const LOG_FILE = join(LOG_DIR, "tokens.jsonl");
|
|
|
11
11
|
// ============================================================================
|
|
12
12
|
let config = DEFAULT_CONFIG;
|
|
13
13
|
let configWarnings = [];
|
|
14
|
+
let lastConfigLoadTime = 0;
|
|
15
|
+
let lastConfigMtime = 0;
|
|
14
16
|
function loadConfig() {
|
|
15
17
|
try {
|
|
16
18
|
if (existsSync(CONFIG_FILE)) {
|
|
@@ -27,6 +29,26 @@ function loadConfig() {
|
|
|
27
29
|
}
|
|
28
30
|
return DEFAULT_CONFIG;
|
|
29
31
|
}
|
|
32
|
+
function ensureLatestConfig() {
|
|
33
|
+
const now = Date.now();
|
|
34
|
+
if (now - lastConfigLoadTime < 2000) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
lastConfigLoadTime = now;
|
|
38
|
+
try {
|
|
39
|
+
if (existsSync(CONFIG_FILE)) {
|
|
40
|
+
const stat = statSync(CONFIG_FILE);
|
|
41
|
+
const mtime = stat.mtimeMs;
|
|
42
|
+
if (mtime !== lastConfigMtime) {
|
|
43
|
+
config = loadConfig();
|
|
44
|
+
lastConfigMtime = mtime;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
// Keep current config on error
|
|
50
|
+
}
|
|
51
|
+
}
|
|
30
52
|
// ============================================================================
|
|
31
53
|
// Pricing
|
|
32
54
|
// ============================================================================
|
|
@@ -270,6 +292,10 @@ export const TokenTrackerPlugin = async ({ directory, client }) => {
|
|
|
270
292
|
try {
|
|
271
293
|
// Load config on plugin init (with validation)
|
|
272
294
|
config = loadConfig();
|
|
295
|
+
lastConfigLoadTime = Date.now();
|
|
296
|
+
if (existsSync(CONFIG_FILE)) {
|
|
297
|
+
lastConfigMtime = statSync(CONFIG_FILE).mtimeMs;
|
|
298
|
+
}
|
|
273
299
|
// Initialize in-memory budget tracker (reads JSONL once)
|
|
274
300
|
initBudgetTracker();
|
|
275
301
|
logJson({ type: "init", directory, configLoaded: existsSync(CONFIG_FILE) });
|
|
@@ -292,6 +318,7 @@ export const TokenTrackerPlugin = async ({ directory, client }) => {
|
|
|
292
318
|
try {
|
|
293
319
|
// Handle message updates (token tracking)
|
|
294
320
|
if (event.type === "message.updated") {
|
|
321
|
+
ensureLatestConfig();
|
|
295
322
|
const props = event.properties;
|
|
296
323
|
const info = props?.info;
|
|
297
324
|
if (!info?.tokens)
|
|
@@ -376,6 +403,7 @@ export const TokenTrackerPlugin = async ({ directory, client }) => {
|
|
|
376
403
|
}
|
|
377
404
|
// Handle session idle (show summary)
|
|
378
405
|
if (event.type === "session.idle") {
|
|
406
|
+
ensureLatestConfig();
|
|
379
407
|
if (!config.toast.enabled || !config.toast.showOnIdle)
|
|
380
408
|
return;
|
|
381
409
|
const props = event.properties;
|
package/package.json
CHANGED