@vibe-cafe/vibe-usage 0.9.16 → 0.10.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.
@@ -0,0 +1,138 @@
1
+ import {
2
+ existsSync,
3
+ mkdirSync,
4
+ readFileSync,
5
+ renameSync,
6
+ rmSync,
7
+ writeFileSync,
8
+ } from 'node:fs';
9
+ import { createHash, randomBytes } from 'node:crypto';
10
+ import { homedir } from 'node:os';
11
+ import { join } from 'node:path';
12
+
13
+ // Parser caches are disposable derived data. Keep their schema/version fully
14
+ // separate from ~/.vibe-usage/state.json, whose hashes are the authoritative
15
+ // record of successful uploads and must remain backward-compatible.
16
+ export const CODEX_CACHE_SCHEMA_VERSION = 1;
17
+ export const CODEX_PARSER_ALGORITHM_VERSION = 1;
18
+
19
+ function hash(value, length = 24) {
20
+ return createHash('sha256').update(value).digest('hex').slice(0, length);
21
+ }
22
+
23
+ export function codexCacheEnabled() {
24
+ return process.env.VIBE_USAGE_CODEX_CACHE !== '0';
25
+ }
26
+
27
+ export function fileSignature(stat) {
28
+ return {
29
+ size: stat.size,
30
+ mtimeMs: stat.mtimeMs,
31
+ dev: String(stat.dev),
32
+ ino: String(stat.ino),
33
+ };
34
+ }
35
+
36
+ function sameSignature(a, b) {
37
+ return a?.size === b.size
38
+ && a?.mtimeMs === b.mtimeMs
39
+ && a?.dev === b.dev
40
+ && a?.ino === b.ino;
41
+ }
42
+
43
+ export function codexCacheDir(codexHome) {
44
+ const base = process.env.VIBE_USAGE_CACHE_DIR?.trim()
45
+ || join(homedir(), '.vibe-usage', 'cache');
46
+ return join(base, 'codex', `root-${hash(codexHome)}`);
47
+ }
48
+
49
+ function entryPath(codexHome, filePath) {
50
+ return join(codexCacheDir(codexHome), `${hash(filePath, 32)}.json`);
51
+ }
52
+
53
+ function tailPath(codexHome, filePath) {
54
+ return join(codexCacheDir(codexHome), `${hash(filePath, 32)}.tail.json`);
55
+ }
56
+
57
+ export function loadCodexFileCache(codexHome, filePath, signature) {
58
+ if (!codexCacheEnabled()) return null;
59
+ const path = entryPath(codexHome, filePath);
60
+ if (!existsSync(path)) return null;
61
+ try {
62
+ const parsed = JSON.parse(readFileSync(path, 'utf8'));
63
+ if (parsed.schemaVersion !== CODEX_CACHE_SCHEMA_VERSION) return null;
64
+ if (parsed.algorithmVersion !== CODEX_PARSER_ALGORITHM_VERSION) return null;
65
+ if (parsed.filePath !== filePath) return null;
66
+ if (signature && !sameSignature(parsed.signature, signature)) return null;
67
+ return parsed;
68
+ } catch {
69
+ // Cache corruption is a performance miss, never a correctness failure.
70
+ return null;
71
+ }
72
+ }
73
+
74
+ export function saveCodexFileCache(codexHome, filePath, signature, data) {
75
+ if (!codexCacheEnabled()) return;
76
+ const dir = codexCacheDir(codexHome);
77
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
78
+ const path = entryPath(codexHome, filePath);
79
+ const tempPath = `${path}.${process.pid}.${randomBytes(6).toString('hex')}.tmp`;
80
+ const payload = {
81
+ schemaVersion: CODEX_CACHE_SCHEMA_VERSION,
82
+ algorithmVersion: CODEX_PARSER_ALGORITHM_VERSION,
83
+ filePath,
84
+ signature,
85
+ ...data,
86
+ };
87
+ try {
88
+ writeFileSync(tempPath, `${JSON.stringify(payload)}\n`, { encoding: 'utf8', mode: 0o600 });
89
+ renameSync(tempPath, path);
90
+ } finally {
91
+ // A killed writer can leave a unique temp file; a normal failed writer
92
+ // should not. rmSync is safe here because the target is this call's exact,
93
+ // random temporary path inside the versioned cache directory.
94
+ rmSync(tempPath, { force: true });
95
+ }
96
+ }
97
+
98
+ export function loadCodexFileTail(codexHome, filePath) {
99
+ if (!codexCacheEnabled()) return null;
100
+ const path = tailPath(codexHome, filePath);
101
+ if (!existsSync(path)) return null;
102
+ try {
103
+ const parsed = JSON.parse(readFileSync(path, 'utf8'));
104
+ if (parsed.schemaVersion !== CODEX_CACHE_SCHEMA_VERSION) return null;
105
+ if (parsed.algorithmVersion !== CODEX_PARSER_ALGORITHM_VERSION) return null;
106
+ if (parsed.filePath !== filePath || !parsed.signature || !parsed.tail) return null;
107
+ return parsed;
108
+ } catch {
109
+ return null;
110
+ }
111
+ }
112
+
113
+ export function saveCodexFileTail(codexHome, filePath, signature, tail) {
114
+ if (!codexCacheEnabled() || !tail) return;
115
+ const dir = codexCacheDir(codexHome);
116
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
117
+ const path = tailPath(codexHome, filePath);
118
+ const tempPath = `${path}.${process.pid}.${randomBytes(6).toString('hex')}.tmp`;
119
+ const payload = {
120
+ schemaVersion: CODEX_CACHE_SCHEMA_VERSION,
121
+ algorithmVersion: CODEX_PARSER_ALGORITHM_VERSION,
122
+ filePath,
123
+ signature,
124
+ tail,
125
+ };
126
+ try {
127
+ writeFileSync(tempPath, `${JSON.stringify(payload)}\n`, { encoding: 'utf8', mode: 0o600 });
128
+ renameSync(tempPath, path);
129
+ } finally {
130
+ rmSync(tempPath, { force: true });
131
+ }
132
+ }
133
+
134
+ export function removeCodexFileCache(codexHome, filePath) {
135
+ if (!codexCacheEnabled()) return;
136
+ rmSync(entryPath(codexHome, filePath), { force: true });
137
+ rmSync(tailPath(codexHome, filePath), { force: true });
138
+ }