vibe-roast 0.98.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/README.md +418 -0
- package/assests/README.md +43 -0
- package/assests/badges/.gitkeep +0 -0
- package/assests/badges/01-builder/builder-badge.png +0 -0
- package/assests/badges/01-builder/builder-badge.svg +4 -0
- package/assests/badges/02-debugger/debugger-badge.png +0 -0
- package/assests/badges/02-debugger/debugger-badge.svg +4 -0
- package/assests/badges/03-architect/architect-badge.png +0 -0
- package/assests/badges/03-architect/architect-badge.svg +4 -0
- package/assests/badges/04-codex-believer/codex-believer-badge.png +0 -0
- package/assests/badges/04-codex-believer/codex-believer-badge.svg +4 -0
- package/assests/badges/05-prompt-priest/prompt-priest-badge.png +0 -0
- package/assests/badges/05-prompt-priest/prompt-priest-badge.svg +4 -0
- package/assests/badges/06-tab-hoarder/tab-hoarder-badge.png +0 -0
- package/assests/badges/06-tab-hoarder/tab-hoarder-badge.svg +4 -0
- package/assests/badges/07-context-maxxer/context-maxxer-badge.png +0 -0
- package/assests/badges/07-context-maxxer/context-maxxer-badge.svg +4 -0
- package/assests/badges/08-yolo-shipper/yolo-shipper-badge.png +0 -0
- package/assests/badges/08-yolo-shipper/yolo-shipper-badge.svg +4 -0
- package/assests/banners/.gitkeep +0 -0
- package/assests/banners/home-hero.png +0 -0
- package/assests/banners/home-hero.svg +3 -0
- package/assests/banners/result-header.png +0 -0
- package/assests/banners/result-header.svg +3 -0
- package/assests/banners/share-strip.png +0 -0
- package/assests/banners/share-strip.svg +3 -0
- package/assests/characters/01-builder/builder-card.sbti-archive.svg +5 -0
- package/assests/characters/01-builder/builder-card.svg +3 -0
- package/assests/characters/01-builder/builder-character.sbti-archive.svg +3 -0
- package/assests/characters/01-builder/builder-character.svg +3 -0
- package/assests/characters/01-builder/builder-figure.png +0 -0
- package/assests/characters/01-builder/builder-mascot.png +0 -0
- package/assests/characters/02-debugger/debugger-card.sbti-archive.svg +5 -0
- package/assests/characters/02-debugger/debugger-card.svg +3 -0
- package/assests/characters/02-debugger/debugger-character.sbti-archive.svg +3 -0
- package/assests/characters/02-debugger/debugger-character.svg +3 -0
- package/assests/characters/02-debugger/debugger-figure.png +0 -0
- package/assests/characters/02-debugger/debugger-mascot.png +0 -0
- package/assests/characters/03-architect/architect-card.sbti-archive.svg +5 -0
- package/assests/characters/03-architect/architect-card.svg +3 -0
- package/assests/characters/03-architect/architect-character.sbti-archive.svg +3 -0
- package/assests/characters/03-architect/architect-character.svg +3 -0
- package/assests/characters/03-architect/architect-figure.png +0 -0
- package/assests/characters/03-architect/architect-mascot.png +0 -0
- package/assests/characters/04-codex-believer/codex-believer-card.sbti-archive.svg +5 -0
- package/assests/characters/04-codex-believer/codex-believer-card.svg +3 -0
- package/assests/characters/04-codex-believer/codex-believer-character.sbti-archive.svg +3 -0
- package/assests/characters/04-codex-believer/codex-believer-character.svg +3 -0
- package/assests/characters/04-codex-believer/codex-believer-figure.png +0 -0
- package/assests/characters/04-codex-believer/codex-believer-mascot.png +0 -0
- package/assests/characters/05-prompt-priest/prompt-priest-card.sbti-archive.svg +5 -0
- package/assests/characters/05-prompt-priest/prompt-priest-card.svg +3 -0
- package/assests/characters/05-prompt-priest/prompt-priest-character.sbti-archive.svg +3 -0
- package/assests/characters/05-prompt-priest/prompt-priest-character.svg +3 -0
- package/assests/characters/05-prompt-priest/prompt-priest-figure.png +0 -0
- package/assests/characters/05-prompt-priest/prompt-priest-mascot.png +0 -0
- package/assests/characters/06-tab-hoarder/tab-hoarder-card.sbti-archive.svg +5 -0
- package/assests/characters/06-tab-hoarder/tab-hoarder-card.svg +3 -0
- package/assests/characters/06-tab-hoarder/tab-hoarder-character.sbti-archive.svg +3 -0
- package/assests/characters/06-tab-hoarder/tab-hoarder-character.svg +3 -0
- package/assests/characters/06-tab-hoarder/tab-hoarder-figure.png +0 -0
- package/assests/characters/06-tab-hoarder/tab-hoarder-mascot.png +0 -0
- package/assests/characters/07-context-maxxer/context-maxxer-card.sbti-archive.svg +5 -0
- package/assests/characters/07-context-maxxer/context-maxxer-card.svg +3 -0
- package/assests/characters/07-context-maxxer/context-maxxer-character.sbti-archive.svg +3 -0
- package/assests/characters/07-context-maxxer/context-maxxer-character.svg +3 -0
- package/assests/characters/07-context-maxxer/context-maxxer-figure.png +0 -0
- package/assests/characters/07-context-maxxer/context-maxxer-mascot.png +0 -0
- package/assests/characters/08-yolo-shipper/yolo-shipper-card.sbti-archive.svg +5 -0
- package/assests/characters/08-yolo-shipper/yolo-shipper-card.svg +3 -0
- package/assests/characters/08-yolo-shipper/yolo-shipper-character.sbti-archive.svg +3 -0
- package/assests/characters/08-yolo-shipper/yolo-shipper-character.svg +3 -0
- package/assests/characters/08-yolo-shipper/yolo-shipper-figure.png +0 -0
- package/assests/characters/08-yolo-shipper/yolo-shipper-mascot.png +0 -0
- package/assests/characters-vibe-types/aosf-strategy-shipper/strategy-shipper-figure.png +0 -0
- package/assests/characters-vibe-types/aosx-agent-believer/agent-believer-figure.png +0 -0
- package/assests/characters-vibe-types/aovf-systems-architect/systems-architect-figure.png +0 -0
- package/assests/characters-vibe-types/aovx-context-cartographer/context-cartographer-figure.png +0 -0
- package/assests/characters-vibe-types/apsf-diagram-sprinter/diagram-sprinter-figure.png +0 -0
- package/assests/characters-vibe-types/apsx-infinite-planner/infinite-planner-figure.png +0 -0
- package/assests/characters-vibe-types/apvf-architect/architect-figure.png +0 -0
- package/assests/characters-vibe-types/apvx-prompt-priest/prompt-priest-figure.png +0 -0
- package/assests/characters-vibe-types/mosf-yolo-shipper/yolo-shipper-figure.png +0 -0
- package/assests/characters-vibe-types/mosx-agent-commander/agent-commander-figure.png +0 -0
- package/assests/characters-vibe-types/movf-agent-engineer/agent-engineer-figure.png +0 -0
- package/assests/characters-vibe-types/movx-systems-wrangler/systems-wrangler-figure.png +0 -0
- package/assests/characters-vibe-types/mpsf-builder/builder-figure.png +0 -0
- package/assests/characters-vibe-types/mpsx-tab-hoarder/tab-hoarder-figure.png +0 -0
- package/assests/characters-vibe-types/mpvf-debugger/debugger-figure.png +0 -0
- package/assests/characters-vibe-types/mpvx-context-maxxer/context-maxxer-figure.png +0 -0
- package/assests/live-report.html +1403 -0
- package/assests/notes/live-inspect-summary.json +3487 -0
- package/assests/scripts/compose-banners.py +148 -0
- package/assests/scripts/live-report-boot.js +2 -0
- package/assests/scripts/profiles.json +104 -0
- package/assests/scripts/qa-faces.mjs +72 -0
- package/assests/scripts/roast-extras.js +619 -0
- package/assests/scripts/score-engine.js +62 -0
- package/assests/scripts/split-badges.py +49 -0
- package/assests/scripts/split-reference.py +38 -0
- package/assests/scripts/sync-banners.mjs +45 -0
- package/assests/scripts/sync-cards.mjs +50 -0
- package/assests/scripts/sync-raster.mjs +118 -0
- package/assests/scripts/vibe-types.json +22 -0
- package/assests/source/design-system.md +50 -0
- package/assests/source/prompts/01-builder.md +19 -0
- package/assests/source/prompts/02-debugger.md +17 -0
- package/assests/source/prompts/03-architect.md +16 -0
- package/assests/source/prompts/04-codex-believer.md +13 -0
- package/assests/source/prompts/05-prompt-priest.md +13 -0
- package/assests/source/prompts/06-tab-hoarder.md +13 -0
- package/assests/source/prompts/07-context-maxxer.md +13 -0
- package/assests/source/prompts/08-yolo-shipper.md +13 -0
- package/assests/source/references/sbti-style-reference-board.png +0 -0
- package/assests/source/references/vibe-badges-v6-board.png +0 -0
- package/assests/source/references/vibe-mascots-v4-reference.png +0 -0
- package/assests/source/references/vibe-profiles-v5-board-a.png +0 -0
- package/assests/source/references/vibe-profiles-v5-board-b.png +0 -0
- package/assests/source/vibe-types-visual-brief.md +38 -0
- package/bin/hook.js +121 -0
- package/bin/vibe-roast.js +98 -0
- package/bin/vibe-wrapper.js +4 -0
- package/dashboard/dist/assets/index-DlBycTcO.css +1 -0
- package/dashboard/dist/assets/index-t8m64Hm9.js +168 -0
- package/dashboard/dist/index.html +32 -0
- package/package.json +58 -0
- package/src/extract/environment.js +160 -0
- package/src/extract/phrase-stats.js +684 -0
- package/src/extract/prompt-analysis.js +368 -0
- package/src/extract/text.js +30 -0
- package/src/hooks/install.js +184 -0
- package/src/inspect.js +261 -0
- package/src/lib/activity-metrics.js +238 -0
- package/src/lib/agent-score.js +187 -0
- package/src/lib/ai-roast.js +446 -0
- package/src/lib/dates.js +43 -0
- package/src/lib/hashtags.js +150 -0
- package/src/lib/jsonl.js +48 -0
- package/src/lib/load-env.js +59 -0
- package/src/lib/roast-evidence.js +186 -0
- package/src/lib/roast-snapshot.js +58 -0
- package/src/server.js +644 -0
- package/src/sources/aider.js +150 -0
- package/src/sources/amazonq.js +113 -0
- package/src/sources/antigravity.js +147 -0
- package/src/sources/claude.js +262 -0
- package/src/sources/cline.js +11 -0
- package/src/sources/codex.js +222 -0
- package/src/sources/common.js +133 -0
- package/src/sources/continue.js +67 -0
- package/src/sources/copilot.js +170 -0
- package/src/sources/cursor.js +238 -0
- package/src/sources/gemini.js +75 -0
- package/src/sources/index.js +61 -0
- package/src/sources/roo.js +11 -0
- package/src/sources/token-tracker.js +171 -0
- package/src/sources/vibe-tracker.js +79 -0
- package/src/sources/vscode-tasks.js +142 -0
- package/src/sources/windsurf.js +109 -0
- package/worker/README.md +96 -0
- package/worker/package-lock.json +1547 -0
- package/worker/package.json +20 -0
- package/worker/src/index.mjs +694 -0
- package/worker/wrangler.jsonc +40 -0
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
const cp = require("node:child_process");
|
|
2
|
+
const os = require("node:os");
|
|
3
|
+
const path = require("node:path");
|
|
4
|
+
|
|
5
|
+
const { isInRange } = require("../lib/dates");
|
|
6
|
+
const { normalizeWhitespace, textFromContent } = require("../extract/text");
|
|
7
|
+
|
|
8
|
+
function resolveCursorDbPath({ home = os.homedir(), platform = process.platform, env = process.env } = {}) {
|
|
9
|
+
if (platform === "darwin") {
|
|
10
|
+
return path.join(home, "Library", "Application Support", "Cursor", "User", "globalStorage", "state.vscdb");
|
|
11
|
+
}
|
|
12
|
+
if (platform === "win32") {
|
|
13
|
+
const appData = env.APPDATA || path.join(home, "AppData", "Roaming");
|
|
14
|
+
return path.win32.join(appData, "Cursor", "User", "globalStorage", "state.vscdb");
|
|
15
|
+
}
|
|
16
|
+
const xdg = env.XDG_CONFIG_HOME || path.join(home, ".config");
|
|
17
|
+
return path.join(xdg, "Cursor", "User", "globalStorage", "state.vscdb");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function inspectCursor({ dbPath, range } = {}) {
|
|
21
|
+
const stateDbPath = dbPath || resolveCursorDbPath();
|
|
22
|
+
const rows = readCursorRows(stateDbPath);
|
|
23
|
+
const allEntries = extractCursorEntriesFromRows(rows);
|
|
24
|
+
const prompts = allEntries.filter((entry) => isInRange(entry.timestamp, range));
|
|
25
|
+
const tokenTotals = emptyTotals();
|
|
26
|
+
for (const prompt of prompts) addTotals(tokenTotals, prompt.tokens);
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
source: "cursor",
|
|
30
|
+
root: stateDbPath,
|
|
31
|
+
files_scanned: rows.length > 0 ? 1 : 0,
|
|
32
|
+
prompt_count: prompts.length,
|
|
33
|
+
token_totals: tokenTotals,
|
|
34
|
+
prompts: prompts.map(({ tokens, ...entry }) => entry),
|
|
35
|
+
notes: buildCursorNotes(rows, prompts),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function buildCursorNotes(rows, prompts) {
|
|
40
|
+
if (rows.length === 0) {
|
|
41
|
+
return [
|
|
42
|
+
"No readable Cursor SQLite rows found. Install sqlite3 or pass --cursor-db to a readable state.vscdb.",
|
|
43
|
+
];
|
|
44
|
+
}
|
|
45
|
+
if (prompts.some((prompt) => !prompt.timestamp)) {
|
|
46
|
+
return [
|
|
47
|
+
"Cursor bubble rows expose prompt text but not reliable timestamps in the compact local query; date-filtered views omit undated Cursor prompts.",
|
|
48
|
+
];
|
|
49
|
+
}
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function readCursorRows(dbPath) {
|
|
54
|
+
const itemSql =
|
|
55
|
+
"SELECT key, value FROM ItemTable WHERE key LIKE '%bubble%' OR key LIKE '%composer%' OR key LIKE '%chat%' LIMIT 20000;";
|
|
56
|
+
const diskSql =
|
|
57
|
+
"SELECT key, json_object('type', json_extract(value, '$.type'), 'text', json_extract(value, '$.text'), 'timestamp', json_extract(value, '$.timestamp'), 'createdAt', json_extract(value, '$.createdAt')) AS value FROM cursorDiskKV WHERE key LIKE 'bubbleId:%' AND json_extract(value, '$.type') = 1 AND length(COALESCE(json_extract(value, '$.text'), '')) > 0 LIMIT 20000;";
|
|
58
|
+
return [...readSqliteRows(dbPath, itemSql), ...readSqliteRows(dbPath, diskSql)];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function readSqliteRows(dbPath, sql) {
|
|
62
|
+
try {
|
|
63
|
+
const out = cp.execFileSync("sqlite3", ["-json", dbPath, sql], {
|
|
64
|
+
encoding: "utf8",
|
|
65
|
+
timeout: 10000,
|
|
66
|
+
maxBuffer: 32 * 1024 * 1024,
|
|
67
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
68
|
+
});
|
|
69
|
+
const parsed = JSON.parse(out || "[]");
|
|
70
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
71
|
+
} catch {
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function extractCursorEntriesFromRows(rows) {
|
|
77
|
+
const entries = [];
|
|
78
|
+
for (const row of rows || []) {
|
|
79
|
+
if (!isCursorPromptKey(row.key)) continue;
|
|
80
|
+
if (isCursorAssistantKey(row.key)) continue;
|
|
81
|
+
const value = parseMaybeJson(row.value);
|
|
82
|
+
const candidates = collectCursorPromptCandidates(value);
|
|
83
|
+
for (const candidate of candidates) {
|
|
84
|
+
const text = normalizeWhitespace(textFromContent(candidate.text));
|
|
85
|
+
if (!looksLikePrompt(text)) continue;
|
|
86
|
+
if (isNonUserNoise(text)) continue;
|
|
87
|
+
entries.push({
|
|
88
|
+
source: "cursor",
|
|
89
|
+
timestamp: candidate.timestamp || value?.timestamp || value?.createdAt || null,
|
|
90
|
+
session_file: row.key || "cursor-state",
|
|
91
|
+
text,
|
|
92
|
+
tokens: normalizeCursorTokens(candidate.usage || value?.usage || value),
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return dedupeEntries(entries);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function isCursorPromptKey(key) {
|
|
100
|
+
return /(bubble|composer|chat|conversation|message)/i.test(String(key || ""));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function isCursorAssistantKey(key) {
|
|
104
|
+
return /(assistant|ai[-_]?message|bot[-_]?reply)/i.test(String(key || ""));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function collectCursorPromptCandidates(value) {
|
|
108
|
+
const out = [];
|
|
109
|
+
visit(value, (node) => {
|
|
110
|
+
if (!node || typeof node !== "object") return;
|
|
111
|
+
if (!isCursorUserNode(node)) return;
|
|
112
|
+
const text = node.text ?? node.content ?? node.message ?? node.prompt;
|
|
113
|
+
if (!text || typeof text === "object") return;
|
|
114
|
+
out.push({
|
|
115
|
+
text,
|
|
116
|
+
timestamp: node.timestamp || node.createdAt || node.time || null,
|
|
117
|
+
usage: node.usage || node.tokenUsage || node.tokens || null,
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
return out;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Require an explicit user signal when role/type is present; reject assistant/system/tool. */
|
|
124
|
+
function isCursorUserNode(node) {
|
|
125
|
+
const type = node.type;
|
|
126
|
+
const role = String(node.role || node.kind || "").toLowerCase();
|
|
127
|
+
const typeStr = String(type ?? "").toLowerCase();
|
|
128
|
+
|
|
129
|
+
if (type === 2 || typeStr === "2") return false;
|
|
130
|
+
if (/(assistant|system|tool|function|model)/i.test(role)) return false;
|
|
131
|
+
if (/(assistant|system|tool|ai)/i.test(String(node.bubbleType || node.capabilityType || ""))) {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
if (/(assistant|system|tool)/i.test(typeStr) && typeStr !== "1") return false;
|
|
135
|
+
|
|
136
|
+
if (type === 1 || typeStr === "1") return true;
|
|
137
|
+
if (/^(user|human|prompt|request)$/.test(role)) return true;
|
|
138
|
+
if (typeStr === "user") return true;
|
|
139
|
+
|
|
140
|
+
// Containers that nest many messages — don't treat the shell as a prompt.
|
|
141
|
+
if (Array.isArray(node.messages) || Array.isArray(node.bubbles) || Array.isArray(node.fullConversationHeadersOnly)) {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Legacy ItemTable bubbles: plain { text } with no role/type.
|
|
146
|
+
if (role || (typeStr && typeStr !== "undefined" && typeStr !== "null")) {
|
|
147
|
+
// Unknown non-user role/type — skip.
|
|
148
|
+
if (!/^(1|user|human|prompt|request|bubble)$/.test(role || typeStr)) return false;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return node.text != null || node.prompt != null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function isNonUserNoise(text) {
|
|
155
|
+
const raw = String(text || "");
|
|
156
|
+
if (/<system_notification>/i.test(raw)) return true;
|
|
157
|
+
if (/^\s*<(?:system|tool_result|tool_call|agent_transcript)[\s>]/i.test(raw)) return true;
|
|
158
|
+
if (/\bThe following task has finished\b/i.test(raw)) return true;
|
|
159
|
+
if (/^\s*\[?(system|assistant|tool)\]?\s*:/i.test(raw)) return true;
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function visit(value, fn, seen = new Set()) {
|
|
164
|
+
if (value == null) return;
|
|
165
|
+
if (typeof value !== "object") return;
|
|
166
|
+
if (seen.has(value)) return;
|
|
167
|
+
seen.add(value);
|
|
168
|
+
fn(value);
|
|
169
|
+
if (Array.isArray(value)) {
|
|
170
|
+
for (const item of value) visit(item, fn, seen);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
for (const item of Object.values(value)) visit(item, fn, seen);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function parseMaybeJson(value) {
|
|
177
|
+
if (value && typeof value === "object") return value;
|
|
178
|
+
if (typeof value !== "string") return null;
|
|
179
|
+
try {
|
|
180
|
+
return JSON.parse(value);
|
|
181
|
+
} catch {
|
|
182
|
+
return { text: value };
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function looksLikePrompt(text) {
|
|
187
|
+
if (!text || text.length < 4) return false;
|
|
188
|
+
if (/^(true|false|null|undefined)$/i.test(text)) return false;
|
|
189
|
+
return /[\p{Letter}\p{Script=Han}]/u.test(text);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function normalizeCursorTokens(raw) {
|
|
193
|
+
const input = number(raw?.inputTokens || raw?.input_tokens || raw?.promptTokens || raw?.tokens_prompt);
|
|
194
|
+
const output = number(raw?.outputTokens || raw?.output_tokens || raw?.generatedTokens || raw?.tokens_generated);
|
|
195
|
+
const total = number(raw?.totalTokens || raw?.total_tokens) || input + output;
|
|
196
|
+
return {
|
|
197
|
+
input_tokens: input,
|
|
198
|
+
cached_input_tokens: 0,
|
|
199
|
+
cache_creation_input_tokens: 0,
|
|
200
|
+
output_tokens: output,
|
|
201
|
+
reasoning_output_tokens: 0,
|
|
202
|
+
total_tokens: total,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function dedupeEntries(entries) {
|
|
207
|
+
const seen = new Set();
|
|
208
|
+
const out = [];
|
|
209
|
+
for (const entry of entries) {
|
|
210
|
+
const key = `${entry.timestamp || ""}|${entry.text}`;
|
|
211
|
+
if (seen.has(key)) continue;
|
|
212
|
+
seen.add(key);
|
|
213
|
+
out.push(entry);
|
|
214
|
+
}
|
|
215
|
+
return out;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function emptyTotals() {
|
|
219
|
+
return {
|
|
220
|
+
input_tokens: 0,
|
|
221
|
+
cached_input_tokens: 0,
|
|
222
|
+
cache_creation_input_tokens: 0,
|
|
223
|
+
output_tokens: 0,
|
|
224
|
+
reasoning_output_tokens: 0,
|
|
225
|
+
total_tokens: 0,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function addTotals(target, delta) {
|
|
230
|
+
for (const key of Object.keys(target)) target[key] += number(delta[key]);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function number(value) {
|
|
234
|
+
const n = Number(value || 0);
|
|
235
|
+
return Number.isFinite(n) && n > 0 ? Math.floor(n) : 0;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
module.exports = { inspectCursor, resolveCursorDbPath, extractCursorEntriesFromRows };
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
const os = require("node:os");
|
|
2
|
+
const path = require("node:path");
|
|
3
|
+
|
|
4
|
+
const { walkFiles } = require("../lib/jsonl");
|
|
5
|
+
const { isInRange } = require("../lib/dates");
|
|
6
|
+
const {
|
|
7
|
+
emptyTotals,
|
|
8
|
+
emptyReport,
|
|
9
|
+
addTotals,
|
|
10
|
+
usageFromObject,
|
|
11
|
+
cleanText,
|
|
12
|
+
toIsoTimestamp,
|
|
13
|
+
readJsonFile,
|
|
14
|
+
} = require("./common");
|
|
15
|
+
|
|
16
|
+
async function inspectGemini({ root, range, home = os.homedir() } = {}) {
|
|
17
|
+
const geminiRoot = root || path.join(home, ".gemini", "tmp");
|
|
18
|
+
const files = await walkFiles(
|
|
19
|
+
geminiRoot,
|
|
20
|
+
(filePath, name) =>
|
|
21
|
+
(name.endsWith(".json") || name.endsWith(".jsonl")) &&
|
|
22
|
+
(/chats?[\\/]/i.test(filePath) || /session/i.test(name) || name.startsWith("session-")),
|
|
23
|
+
);
|
|
24
|
+
const prompts = [];
|
|
25
|
+
const tokenTotals = emptyTotals();
|
|
26
|
+
|
|
27
|
+
for (const file of files) {
|
|
28
|
+
if (file.endsWith(".jsonl")) {
|
|
29
|
+
// Rare JSONL exports under gemini tmp — reuse Continue-like objects via readJsonFile line-by-line isn't needed often.
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
const data = await readJsonFile(file);
|
|
33
|
+
if (!data) continue;
|
|
34
|
+
const messages = Array.isArray(data.messages)
|
|
35
|
+
? data.messages
|
|
36
|
+
: Array.isArray(data.history)
|
|
37
|
+
? data.history
|
|
38
|
+
: Array.isArray(data)
|
|
39
|
+
? data
|
|
40
|
+
: [];
|
|
41
|
+
|
|
42
|
+
for (const row of messages) {
|
|
43
|
+
const text = extractGeminiUserText(row);
|
|
44
|
+
if (!text) continue;
|
|
45
|
+
const timestamp = toIsoTimestamp(
|
|
46
|
+
row.timestamp || row.time || row.createdAt || data.lastUpdated || data.startTime || data.sessionId,
|
|
47
|
+
);
|
|
48
|
+
if (!isInRange(timestamp, range)) continue;
|
|
49
|
+
prompts.push({
|
|
50
|
+
source: "gemini",
|
|
51
|
+
timestamp,
|
|
52
|
+
session_file: file,
|
|
53
|
+
text,
|
|
54
|
+
});
|
|
55
|
+
addTotals(tokenTotals, usageFromObject(row));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
...(emptyReport("gemini", geminiRoot)),
|
|
61
|
+
files_scanned: files.length,
|
|
62
|
+
prompt_count: prompts.length,
|
|
63
|
+
token_totals: tokenTotals,
|
|
64
|
+
prompts,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function extractGeminiUserText(row) {
|
|
69
|
+
if (!row || typeof row !== "object") return "";
|
|
70
|
+
const type = String(row.type || row.role || "").toLowerCase();
|
|
71
|
+
if (type && !["user", "human", "input"].includes(type)) return "";
|
|
72
|
+
return cleanText(row.content ?? row.text ?? row.message ?? row.parts ?? "");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
module.exports = { inspectGemini, extractGeminiUserText };
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
const { inspectCodex } = require("./codex");
|
|
2
|
+
const { inspectClaude } = require("./claude");
|
|
3
|
+
const { inspectCursor } = require("./cursor");
|
|
4
|
+
const { inspectVibeTracker } = require("./vibe-tracker");
|
|
5
|
+
const { inspectCline } = require("./cline");
|
|
6
|
+
const { inspectRoo } = require("./roo");
|
|
7
|
+
const { inspectContinue } = require("./continue");
|
|
8
|
+
const { inspectGemini } = require("./gemini");
|
|
9
|
+
const { inspectAider } = require("./aider");
|
|
10
|
+
const { inspectWindsurf } = require("./windsurf");
|
|
11
|
+
const { inspectCopilot } = require("./copilot");
|
|
12
|
+
const { inspectAmazonQ } = require("./amazonq");
|
|
13
|
+
const { inspectAntigravity } = require("./antigravity");
|
|
14
|
+
|
|
15
|
+
const SOURCE_INSPECTORS = {
|
|
16
|
+
codex: inspectCodex,
|
|
17
|
+
claude: inspectClaude,
|
|
18
|
+
cursor: inspectCursor,
|
|
19
|
+
"vibe-tracker": inspectVibeTracker,
|
|
20
|
+
cline: inspectCline,
|
|
21
|
+
roo: inspectRoo,
|
|
22
|
+
continue: inspectContinue,
|
|
23
|
+
gemini: inspectGemini,
|
|
24
|
+
aider: inspectAider,
|
|
25
|
+
windsurf: inspectWindsurf,
|
|
26
|
+
copilot: inspectCopilot,
|
|
27
|
+
amazonq: inspectAmazonQ,
|
|
28
|
+
antigravity: inspectAntigravity,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/** Primary + best-effort mainstream local agents. Missing dirs return empty counts. */
|
|
32
|
+
const DEFAULT_SOURCES = [
|
|
33
|
+
"codex",
|
|
34
|
+
"claude",
|
|
35
|
+
"cursor",
|
|
36
|
+
"cline",
|
|
37
|
+
"roo",
|
|
38
|
+
"continue",
|
|
39
|
+
"gemini",
|
|
40
|
+
"aider",
|
|
41
|
+
"windsurf",
|
|
42
|
+
"copilot",
|
|
43
|
+
"amazonq",
|
|
44
|
+
"antigravity",
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
const KNOWN_SOURCES = Object.keys(SOURCE_INSPECTORS);
|
|
48
|
+
|
|
49
|
+
function normalizeSources(sources) {
|
|
50
|
+
if (typeof sources === "string") {
|
|
51
|
+
return sources.split(",").map((s) => s.trim()).filter(Boolean);
|
|
52
|
+
}
|
|
53
|
+
return Array.isArray(sources) && sources.length > 0 ? sources : DEFAULT_SOURCES.slice();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
module.exports = {
|
|
57
|
+
SOURCE_INSPECTORS,
|
|
58
|
+
DEFAULT_SOURCES,
|
|
59
|
+
KNOWN_SOURCES,
|
|
60
|
+
normalizeSources,
|
|
61
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
const { inspectVscodeTaskStore, extractTaskUserText } = require("./vscode-tasks");
|
|
2
|
+
|
|
3
|
+
async function inspectRoo(options = {}) {
|
|
4
|
+
return inspectVscodeTaskStore({
|
|
5
|
+
source: "roo",
|
|
6
|
+
extensionId: "rooveterinaryinc.roo-cline",
|
|
7
|
+
...options,
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
module.exports = { inspectRoo, extractTaskUserText };
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
const fs = require("node:fs");
|
|
2
|
+
const path = require("node:path");
|
|
3
|
+
const os = require("node:os");
|
|
4
|
+
const { createInterface } = require("node:readline");
|
|
5
|
+
|
|
6
|
+
const DEFAULT_QUEUE = path.join(os.homedir(), ".tokentracker", "tracker", "queue.jsonl");
|
|
7
|
+
|
|
8
|
+
async function inspectTokenTracker({ queuePath, range } = {}) {
|
|
9
|
+
const file = queuePath || DEFAULT_QUEUE;
|
|
10
|
+
let filesScanned = 0;
|
|
11
|
+
const buckets = new Map();
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
filesScanned = 1;
|
|
15
|
+
const rl = createInterface({
|
|
16
|
+
input: fs.createReadStream(file, "utf8"),
|
|
17
|
+
crlfDelay: Infinity,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
for await (const line of rl) {
|
|
21
|
+
if (!line.trim()) continue;
|
|
22
|
+
let row;
|
|
23
|
+
try {
|
|
24
|
+
row = JSON.parse(line);
|
|
25
|
+
} catch {
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const timestamp = row.hour_start || row.timestamp || row.ended_at || row.created_at;
|
|
30
|
+
if (!timestamp || !inRange(timestamp, range)) continue;
|
|
31
|
+
|
|
32
|
+
const totals = normalizeTotals(row);
|
|
33
|
+
if (totals.total_tokens <= 0 && totals.billable_total_tokens <= 0) continue;
|
|
34
|
+
|
|
35
|
+
const source = normalizeText(row.source, "unknown").toLowerCase();
|
|
36
|
+
const model = normalizeText(row.model, "unknown");
|
|
37
|
+
buckets.set(`${source}|${model}|${timestamp}`, {
|
|
38
|
+
...row,
|
|
39
|
+
source,
|
|
40
|
+
model,
|
|
41
|
+
hour_start: timestamp,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
} catch {
|
|
45
|
+
return emptyReport(file, filesScanned);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const daily = new Map();
|
|
49
|
+
const tokenTotals = emptyTotals();
|
|
50
|
+
|
|
51
|
+
for (const row of buckets.values()) {
|
|
52
|
+
const timestamp = row.hour_start;
|
|
53
|
+
const totals = normalizeTotals(row);
|
|
54
|
+
addTotals(tokenTotals, totals);
|
|
55
|
+
|
|
56
|
+
const day = String(timestamp).slice(0, 10);
|
|
57
|
+
const current = daily.get(day) || {
|
|
58
|
+
day,
|
|
59
|
+
total_tokens: 0,
|
|
60
|
+
billable_total_tokens: 0,
|
|
61
|
+
input_tokens: 0,
|
|
62
|
+
cached_input_tokens: 0,
|
|
63
|
+
cache_creation_input_tokens: 0,
|
|
64
|
+
output_tokens: 0,
|
|
65
|
+
reasoning_output_tokens: 0,
|
|
66
|
+
conversation_count: 0,
|
|
67
|
+
models: {},
|
|
68
|
+
sources: {},
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
addTotals(current, totals);
|
|
72
|
+
current.conversation_count += number(row.conversation_count);
|
|
73
|
+
|
|
74
|
+
const source = row.source;
|
|
75
|
+
const model = row.model;
|
|
76
|
+
const modelKey = `${source}/${model}`;
|
|
77
|
+
const value = totals.billable_total_tokens || totals.total_tokens;
|
|
78
|
+
current.models[modelKey] = (current.models[modelKey] || 0) + value;
|
|
79
|
+
current.sources[source] = (current.sources[source] || 0) + value;
|
|
80
|
+
daily.set(day, current);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const dailyRows = Array.from(daily.values()).sort((a, b) => a.day.localeCompare(b.day));
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
source: "token-tracker",
|
|
87
|
+
root: file,
|
|
88
|
+
files_scanned: filesScanned,
|
|
89
|
+
bucket_count: buckets.size,
|
|
90
|
+
active_day_count: dailyRows.filter((row) => (row.billable_total_tokens || row.total_tokens) > 0).length,
|
|
91
|
+
token_totals: publicTotals(tokenTotals),
|
|
92
|
+
daily_rows: dailyRows.map((row) => ({
|
|
93
|
+
...row,
|
|
94
|
+
value: row.billable_total_tokens || row.total_tokens,
|
|
95
|
+
level_value: row.billable_total_tokens || row.total_tokens,
|
|
96
|
+
})),
|
|
97
|
+
prompts: [],
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function normalizeText(value, fallback) {
|
|
102
|
+
return typeof value === "string" && value.trim() ? value.trim() : fallback;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function emptyReport(file, filesScanned = 0) {
|
|
106
|
+
return {
|
|
107
|
+
source: "token-tracker",
|
|
108
|
+
root: file,
|
|
109
|
+
files_scanned: filesScanned,
|
|
110
|
+
bucket_count: 0,
|
|
111
|
+
active_day_count: 0,
|
|
112
|
+
token_totals: publicTotals(emptyTotals()),
|
|
113
|
+
daily_rows: [],
|
|
114
|
+
prompts: [],
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function inRange(timestamp, range) {
|
|
119
|
+
if (!range) return true;
|
|
120
|
+
const ms = Date.parse(timestamp);
|
|
121
|
+
if (!Number.isFinite(ms)) return true;
|
|
122
|
+
const fromMs = range.fromMs ?? (range.from ? Date.parse(range.from) : null);
|
|
123
|
+
const toMs = range.toMs ?? (range.to ? Date.parse(range.to) : null);
|
|
124
|
+
if (Number.isFinite(fromMs) && ms < fromMs) return false;
|
|
125
|
+
if (Number.isFinite(toMs) && ms > toMs) return false;
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function normalizeTotals(row) {
|
|
130
|
+
const totals = {
|
|
131
|
+
input_tokens: number(row.input_tokens),
|
|
132
|
+
cached_input_tokens: number(row.cached_input_tokens),
|
|
133
|
+
cache_creation_input_tokens: number(row.cache_creation_input_tokens),
|
|
134
|
+
output_tokens: number(row.output_tokens),
|
|
135
|
+
reasoning_output_tokens: number(row.reasoning_output_tokens),
|
|
136
|
+
total_tokens: number(row.total_tokens),
|
|
137
|
+
billable_total_tokens: number(row.billable_total_tokens),
|
|
138
|
+
};
|
|
139
|
+
if (!totals.billable_total_tokens) totals.billable_total_tokens = totals.total_tokens;
|
|
140
|
+
return totals;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function emptyTotals() {
|
|
144
|
+
return {
|
|
145
|
+
input_tokens: 0,
|
|
146
|
+
cached_input_tokens: 0,
|
|
147
|
+
cache_creation_input_tokens: 0,
|
|
148
|
+
output_tokens: 0,
|
|
149
|
+
reasoning_output_tokens: 0,
|
|
150
|
+
total_tokens: 0,
|
|
151
|
+
billable_total_tokens: 0,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function publicTotals(totals) {
|
|
156
|
+
const { billable_total_tokens: _billable, ...rest } = totals;
|
|
157
|
+
return rest;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function addTotals(target, delta) {
|
|
161
|
+
for (const key of Object.keys(delta)) {
|
|
162
|
+
target[key] = number(target[key]) + number(delta[key]);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function number(value) {
|
|
167
|
+
const n = Number(value || 0);
|
|
168
|
+
return Number.isFinite(n) && n > 0 ? Math.floor(n) : 0;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
module.exports = { inspectTokenTracker };
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
const fs = require("node:fs");
|
|
2
|
+
const path = require("node:path");
|
|
3
|
+
const os = require("node:os");
|
|
4
|
+
const { createInterface } = require("node:readline");
|
|
5
|
+
|
|
6
|
+
const STORE_DIR = path.join(os.homedir(), ".vibe-wrapper");
|
|
7
|
+
|
|
8
|
+
async function inspectVibeTracker({ range } = {}) {
|
|
9
|
+
const storeFile = path.join(STORE_DIR, "sessions.jsonl");
|
|
10
|
+
const prompts = [];
|
|
11
|
+
let tokenTotals = emptyTotals();
|
|
12
|
+
let filesScanned = 0;
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
filesScanned = 1;
|
|
16
|
+
const stream = fs.createReadStream(storeFile, "utf8");
|
|
17
|
+
const rl = createInterface({ input: stream, crlfDelay: Infinity });
|
|
18
|
+
|
|
19
|
+
for await (const line of rl) {
|
|
20
|
+
if (!line.trim()) continue;
|
|
21
|
+
try {
|
|
22
|
+
const record = JSON.parse(line);
|
|
23
|
+
if (!inRange(record.ended_at, range)) continue;
|
|
24
|
+
|
|
25
|
+
tokenTotals.input_tokens += Number(record.input_tokens || 0);
|
|
26
|
+
tokenTotals.output_tokens += Number(record.output_tokens || 0);
|
|
27
|
+
tokenTotals.cached_input_tokens += Number(record.cached_input_tokens || 0);
|
|
28
|
+
tokenTotals.cache_creation_input_tokens += Number(record.cache_creation_input_tokens || 0);
|
|
29
|
+
tokenTotals.total_tokens += Number(record.total_tokens || 0);
|
|
30
|
+
|
|
31
|
+
prompts.push({
|
|
32
|
+
source: "vibe-tracker",
|
|
33
|
+
timestamp: record.ended_at,
|
|
34
|
+
text: `Session by ${record.source}: ${record.model || "unknown"} — ${formatTokens(record.total_tokens)} tokens`,
|
|
35
|
+
input_tokens: record.input_tokens,
|
|
36
|
+
output_tokens: record.output_tokens,
|
|
37
|
+
cached_input_tokens: record.cached_input_tokens,
|
|
38
|
+
cache_creation_input_tokens: record.cache_creation_input_tokens,
|
|
39
|
+
});
|
|
40
|
+
} catch { /* skip */ }
|
|
41
|
+
}
|
|
42
|
+
} catch {
|
|
43
|
+
// No data yet — return empty
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
files_scanned: filesScanned,
|
|
48
|
+
prompt_count: prompts.length,
|
|
49
|
+
token_totals: tokenTotals,
|
|
50
|
+
prompts,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function inRange(timestamp, range) {
|
|
55
|
+
if (!timestamp || !range) return true;
|
|
56
|
+
if (range.from && timestamp < range.from + "T00:00:00.000Z") return false;
|
|
57
|
+
if (range.to && timestamp > range.to + "T23:59:59.999Z") return false;
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function emptyTotals() {
|
|
62
|
+
return {
|
|
63
|
+
input_tokens: 0,
|
|
64
|
+
cached_input_tokens: 0,
|
|
65
|
+
cache_creation_input_tokens: 0,
|
|
66
|
+
output_tokens: 0,
|
|
67
|
+
reasoning_output_tokens: 0,
|
|
68
|
+
total_tokens: 0,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function formatTokens(n) {
|
|
73
|
+
n = Number(n || 0);
|
|
74
|
+
if (n >= 1e6) return (n / 1e6).toFixed(1) + "M";
|
|
75
|
+
if (n >= 1e3) return (n / 1e3).toFixed(1) + "K";
|
|
76
|
+
return String(n);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
module.exports = { inspectVibeTracker };
|