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,222 @@
|
|
|
1
|
+
const os = require("node:os");
|
|
2
|
+
const path = require("node:path");
|
|
3
|
+
|
|
4
|
+
const { walkFiles, readJsonl } = require("../lib/jsonl");
|
|
5
|
+
const { isInRange } = require("../lib/dates");
|
|
6
|
+
const { normalizeWhitespace, textFromContent } = require("../extract/text");
|
|
7
|
+
|
|
8
|
+
async function inspectCodex({ root, range } = {}) {
|
|
9
|
+
const sessionsRoot = root || path.join(process.env.CODEX_HOME || path.join(os.homedir(), ".codex"), "sessions");
|
|
10
|
+
const files = await walkFiles(
|
|
11
|
+
sessionsRoot,
|
|
12
|
+
(_filePath, name) => name.endsWith(".jsonl") && (name.startsWith("rollout-") || true),
|
|
13
|
+
);
|
|
14
|
+
const prompts = [];
|
|
15
|
+
const tokenTotals = emptyTotals();
|
|
16
|
+
const contextByDay = new Map();
|
|
17
|
+
|
|
18
|
+
for (const file of files) {
|
|
19
|
+
let previousTotals = null;
|
|
20
|
+
let pendingTools = [];
|
|
21
|
+
await readJsonl(file, (obj) => {
|
|
22
|
+
const timestamp = obj.timestamp || obj.payload?.timestamp || null;
|
|
23
|
+
const inRequestedRange = isInRange(timestamp, range);
|
|
24
|
+
|
|
25
|
+
if (inRequestedRange) {
|
|
26
|
+
const text = extractCodexPrompt(obj);
|
|
27
|
+
if (text) {
|
|
28
|
+
prompts.push({
|
|
29
|
+
source: "codex",
|
|
30
|
+
timestamp,
|
|
31
|
+
session_file: file,
|
|
32
|
+
text,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const tokens = extractCodexTokens(obj);
|
|
37
|
+
if (tokens) addTotals(tokenTotals, tokens);
|
|
38
|
+
|
|
39
|
+
const tool = extractCodexTool(obj);
|
|
40
|
+
if (tool) pendingTools.push(tool);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const usage = extractCodexUsageEvent(obj);
|
|
44
|
+
if (usage) {
|
|
45
|
+
const delta = codexUsageDelta(usage, previousTotals);
|
|
46
|
+
if (usage.total) previousTotals = usage.total;
|
|
47
|
+
if (inRequestedRange) addCodexContext(contextByDay, timestamp, delta, pendingTools);
|
|
48
|
+
pendingTools = [];
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
source: "codex",
|
|
55
|
+
root: sessionsRoot,
|
|
56
|
+
files_scanned: files.length,
|
|
57
|
+
prompt_count: prompts.length,
|
|
58
|
+
token_totals: tokenTotals,
|
|
59
|
+
context_breakdown_daily: [...contextByDay.values()].sort((a, b) => a.day.localeCompare(b.day)),
|
|
60
|
+
prompts,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function extractCodexPrompt(obj) {
|
|
65
|
+
const payload = obj?.payload || {};
|
|
66
|
+
const nested = payload.msg && typeof payload.msg === "object" ? payload.msg : null;
|
|
67
|
+
const type = String(payload.type || nested?.type || "").toLowerCase();
|
|
68
|
+
// Codex rollouts mix user / agent / tool / token events — only keep user prompts.
|
|
69
|
+
if (type !== "user_message") return "";
|
|
70
|
+
|
|
71
|
+
const candidates = [
|
|
72
|
+
payload.message,
|
|
73
|
+
payload.text,
|
|
74
|
+
payload.input,
|
|
75
|
+
payload.content,
|
|
76
|
+
nested?.message,
|
|
77
|
+
nested?.text,
|
|
78
|
+
nested?.input,
|
|
79
|
+
nested?.content,
|
|
80
|
+
];
|
|
81
|
+
for (const candidate of candidates) {
|
|
82
|
+
const text = normalizeWhitespace(textFromContent(candidate));
|
|
83
|
+
if (text) return text;
|
|
84
|
+
}
|
|
85
|
+
return "";
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function extractCodexTokens(obj) {
|
|
89
|
+
const payload = obj?.payload || {};
|
|
90
|
+
const info =
|
|
91
|
+
payload.type === "token_count" ? payload.info
|
|
92
|
+
: payload.msg?.type === "token_count" ? payload.msg.info
|
|
93
|
+
: null;
|
|
94
|
+
if (!info) return null;
|
|
95
|
+
return normalizeTotals(info.total_token_usage || info);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function extractCodexUsageEvent(obj) {
|
|
99
|
+
const payload = obj?.payload || {};
|
|
100
|
+
const info =
|
|
101
|
+
payload.type === "token_count" ? payload.info
|
|
102
|
+
: payload.msg?.type === "token_count" ? payload.msg.info
|
|
103
|
+
: null;
|
|
104
|
+
if (!info) return null;
|
|
105
|
+
return {
|
|
106
|
+
last: info.last_token_usage && typeof info.last_token_usage === "object"
|
|
107
|
+
? info.last_token_usage
|
|
108
|
+
: null,
|
|
109
|
+
total: info.total_token_usage && typeof info.total_token_usage === "object"
|
|
110
|
+
? info.total_token_usage
|
|
111
|
+
: null,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function extractCodexTool(obj) {
|
|
116
|
+
if (obj?.type !== "response_item" || obj?.payload?.type !== "function_call") return "";
|
|
117
|
+
const payload = obj.payload;
|
|
118
|
+
if (typeof payload.namespace === "string" && payload.namespace.startsWith("mcp__")) {
|
|
119
|
+
return `${payload.namespace}${payload.name || ""}`;
|
|
120
|
+
}
|
|
121
|
+
return typeof payload.name === "string" ? payload.name : "";
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function codexUsageDelta(usage, previousTotals) {
|
|
125
|
+
if (!usage) return null;
|
|
126
|
+
if (usage.total && previousTotals) {
|
|
127
|
+
const total = normalizeCodexUsage(usage.total);
|
|
128
|
+
const previous = normalizeCodexUsage(previousTotals);
|
|
129
|
+
if (total.total_tokens >= previous.total_tokens) {
|
|
130
|
+
return Object.fromEntries(
|
|
131
|
+
Object.keys(total).map((key) => [key, Math.max(0, total[key] - previous[key])]),
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return normalizeCodexUsage(usage.last || usage.total || {});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function normalizeCodexUsage(raw) {
|
|
139
|
+
const totals = normalizeTotals(raw);
|
|
140
|
+
// Codex reports input inclusive of cache reads.
|
|
141
|
+
totals.input_tokens = Math.max(0, totals.input_tokens - totals.cached_input_tokens);
|
|
142
|
+
totals.total_tokens =
|
|
143
|
+
totals.input_tokens +
|
|
144
|
+
totals.cached_input_tokens +
|
|
145
|
+
totals.cache_creation_input_tokens +
|
|
146
|
+
totals.output_tokens;
|
|
147
|
+
return totals;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function addCodexContext(byDay, timestamp, delta, tools) {
|
|
151
|
+
if (!delta?.total_tokens || !timestamp) return;
|
|
152
|
+
const day = String(timestamp).slice(0, 10);
|
|
153
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(day)) return;
|
|
154
|
+
const row = byDay.get(day) || emptyContextRow(day, "turn_delta");
|
|
155
|
+
const uniqueTools = [...new Set((tools || []).filter(Boolean))];
|
|
156
|
+
const toolShare = uniqueTools.length > 0 ? 1 : 0;
|
|
157
|
+
const reasoning = Math.min(delta.total_tokens, number(delta.reasoning_output_tokens));
|
|
158
|
+
const attributable = Math.max(0, delta.total_tokens - reasoning);
|
|
159
|
+
row.categories.reasoning += reasoning;
|
|
160
|
+
row.categories.tool_calls += attributable * toolShare;
|
|
161
|
+
row.categories.messages += attributable * (1 - toolShare);
|
|
162
|
+
row.total_tokens += delta.total_tokens;
|
|
163
|
+
row.input_tokens += number(delta.input_tokens)
|
|
164
|
+
+ number(delta.cached_input_tokens)
|
|
165
|
+
+ number(delta.cache_creation_input_tokens);
|
|
166
|
+
row.cached_input_tokens += number(delta.cached_input_tokens);
|
|
167
|
+
row.tool_call_count += uniqueTools.length;
|
|
168
|
+
row.event_count += 1;
|
|
169
|
+
byDay.set(day, row);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function emptyContextRow(day, method) {
|
|
173
|
+
return {
|
|
174
|
+
day,
|
|
175
|
+
method,
|
|
176
|
+
categories: {
|
|
177
|
+
messages: 0,
|
|
178
|
+
tool_calls: 0,
|
|
179
|
+
reasoning: 0,
|
|
180
|
+
system_prompt: 0,
|
|
181
|
+
custom_agents: 0,
|
|
182
|
+
},
|
|
183
|
+
total_tokens: 0,
|
|
184
|
+
input_tokens: 0,
|
|
185
|
+
cached_input_tokens: 0,
|
|
186
|
+
tool_call_count: 0,
|
|
187
|
+
event_count: 0,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function normalizeTotals(raw) {
|
|
192
|
+
return {
|
|
193
|
+
input_tokens: number(raw.input_tokens),
|
|
194
|
+
cached_input_tokens: number(raw.cached_input_tokens),
|
|
195
|
+
cache_creation_input_tokens: number(raw.cache_creation_input_tokens),
|
|
196
|
+
output_tokens: number(raw.output_tokens),
|
|
197
|
+
reasoning_output_tokens: number(raw.reasoning_output_tokens),
|
|
198
|
+
total_tokens: number(raw.total_tokens),
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function emptyTotals() {
|
|
203
|
+
return normalizeTotals({});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function addTotals(target, delta) {
|
|
207
|
+
for (const key of Object.keys(target)) target[key] += number(delta[key]);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function number(value) {
|
|
211
|
+
const n = Number(value || 0);
|
|
212
|
+
return Number.isFinite(n) && n > 0 ? Math.floor(n) : 0;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
module.exports = {
|
|
216
|
+
inspectCodex,
|
|
217
|
+
extractCodexPrompt,
|
|
218
|
+
extractCodexTokens,
|
|
219
|
+
extractCodexTool,
|
|
220
|
+
extractCodexUsageEvent,
|
|
221
|
+
codexUsageDelta,
|
|
222
|
+
};
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
const fs = require("node:fs/promises");
|
|
2
|
+
const path = require("node:path");
|
|
3
|
+
const os = require("node:os");
|
|
4
|
+
|
|
5
|
+
const { normalizeWhitespace, textFromContent } = require("../extract/text");
|
|
6
|
+
|
|
7
|
+
function emptyTotals() {
|
|
8
|
+
return {
|
|
9
|
+
input_tokens: 0,
|
|
10
|
+
cached_input_tokens: 0,
|
|
11
|
+
cache_creation_input_tokens: 0,
|
|
12
|
+
output_tokens: 0,
|
|
13
|
+
reasoning_output_tokens: 0,
|
|
14
|
+
total_tokens: 0,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function number(value) {
|
|
19
|
+
const n = Number(value || 0);
|
|
20
|
+
return Number.isFinite(n) && n > 0 ? Math.floor(n) : 0;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function addTotals(target, delta) {
|
|
24
|
+
if (!delta) return;
|
|
25
|
+
for (const key of Object.keys(target)) {
|
|
26
|
+
target[key] += number(delta[key]);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function normalizeTotals(raw = {}) {
|
|
31
|
+
return {
|
|
32
|
+
input_tokens: number(raw.input_tokens),
|
|
33
|
+
cached_input_tokens: number(raw.cached_input_tokens || raw.cache_read_input_tokens),
|
|
34
|
+
cache_creation_input_tokens: number(raw.cache_creation_input_tokens),
|
|
35
|
+
output_tokens: number(raw.output_tokens),
|
|
36
|
+
reasoning_output_tokens: number(raw.reasoning_output_tokens),
|
|
37
|
+
total_tokens:
|
|
38
|
+
number(raw.total_tokens) ||
|
|
39
|
+
number(raw.input_tokens) +
|
|
40
|
+
number(raw.cached_input_tokens || raw.cache_read_input_tokens) +
|
|
41
|
+
number(raw.cache_creation_input_tokens) +
|
|
42
|
+
number(raw.output_tokens) +
|
|
43
|
+
number(raw.reasoning_output_tokens),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function emptyReport(source, root, notes = []) {
|
|
48
|
+
return {
|
|
49
|
+
source,
|
|
50
|
+
root,
|
|
51
|
+
files_scanned: 0,
|
|
52
|
+
prompt_count: 0,
|
|
53
|
+
token_totals: emptyTotals(),
|
|
54
|
+
prompts: [],
|
|
55
|
+
notes,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function cleanText(value) {
|
|
60
|
+
return normalizeWhitespace(textFromContent(value) || String(value || ""));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function toIsoTimestamp(value) {
|
|
64
|
+
if (value == null || value === "") return null;
|
|
65
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
66
|
+
const ms = value < 1e12 ? value * 1000 : value;
|
|
67
|
+
const date = new Date(ms);
|
|
68
|
+
return Number.isFinite(date.getTime()) ? date.toISOString() : null;
|
|
69
|
+
}
|
|
70
|
+
const raw = String(value).trim();
|
|
71
|
+
if (!raw) return null;
|
|
72
|
+
if (/^\d+$/.test(raw)) return toIsoTimestamp(Number(raw));
|
|
73
|
+
const ms = Date.parse(raw);
|
|
74
|
+
return Number.isFinite(ms) ? new Date(ms).toISOString() : null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function vscodeGlobalStorage(...parts) {
|
|
78
|
+
return resolveVscodeGlobalStorage({ parts });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function resolveVscodeGlobalStorage({
|
|
82
|
+
home = os.homedir(),
|
|
83
|
+
platform = process.platform,
|
|
84
|
+
env = process.env,
|
|
85
|
+
parts = [],
|
|
86
|
+
appName = "Code",
|
|
87
|
+
} = {}) {
|
|
88
|
+
if (platform === "darwin") {
|
|
89
|
+
return path.join(home, "Library", "Application Support", appName, "User", "globalStorage", ...parts);
|
|
90
|
+
}
|
|
91
|
+
if (platform === "win32") {
|
|
92
|
+
const appData = env.APPDATA || path.join(home, "AppData", "Roaming");
|
|
93
|
+
return path.win32.join(appData, appName, "User", "globalStorage", ...parts);
|
|
94
|
+
}
|
|
95
|
+
const xdg = env.XDG_CONFIG_HOME || path.join(home, ".config");
|
|
96
|
+
return path.join(xdg, appName, "User", "globalStorage", ...parts);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function readJsonFile(filePath) {
|
|
100
|
+
try {
|
|
101
|
+
const raw = await fs.readFile(filePath, "utf8");
|
|
102
|
+
return JSON.parse(raw);
|
|
103
|
+
} catch {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function usageFromObject(obj) {
|
|
109
|
+
if (!obj || typeof obj !== "object") return null;
|
|
110
|
+
const usage =
|
|
111
|
+
obj.usage ||
|
|
112
|
+
obj.token_usage ||
|
|
113
|
+
obj.tokens ||
|
|
114
|
+
obj.message?.usage ||
|
|
115
|
+
obj.metrics?.tokens ||
|
|
116
|
+
null;
|
|
117
|
+
if (!usage) return null;
|
|
118
|
+
return normalizeTotals(usage);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
module.exports = {
|
|
122
|
+
emptyTotals,
|
|
123
|
+
emptyReport,
|
|
124
|
+
number,
|
|
125
|
+
addTotals,
|
|
126
|
+
normalizeTotals,
|
|
127
|
+
cleanText,
|
|
128
|
+
toIsoTimestamp,
|
|
129
|
+
vscodeGlobalStorage,
|
|
130
|
+
resolveVscodeGlobalStorage,
|
|
131
|
+
readJsonFile,
|
|
132
|
+
usageFromObject,
|
|
133
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
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 inspectContinue({ root, range, home = os.homedir() } = {}) {
|
|
17
|
+
const sessionsRoot = root || path.join(home, ".continue", "sessions");
|
|
18
|
+
const files = await walkFiles(sessionsRoot, (_filePath, name) => name.endsWith(".json"));
|
|
19
|
+
const prompts = [];
|
|
20
|
+
const tokenTotals = emptyTotals();
|
|
21
|
+
|
|
22
|
+
for (const file of files) {
|
|
23
|
+
const data = await readJsonFile(file);
|
|
24
|
+
if (!data) continue;
|
|
25
|
+
const history = Array.isArray(data.history)
|
|
26
|
+
? data.history
|
|
27
|
+
: Array.isArray(data.messages)
|
|
28
|
+
? data.messages
|
|
29
|
+
: Array.isArray(data)
|
|
30
|
+
? data
|
|
31
|
+
: [];
|
|
32
|
+
|
|
33
|
+
for (const row of history) {
|
|
34
|
+
const text = extractContinueUserText(row);
|
|
35
|
+
if (!text) continue;
|
|
36
|
+
const timestamp =
|
|
37
|
+
toIsoTimestamp(row.timestamp || row.time || row.dateCreated || data.dateCreated || data.createdAt) ||
|
|
38
|
+
null;
|
|
39
|
+
if (!isInRange(timestamp, range)) continue;
|
|
40
|
+
prompts.push({
|
|
41
|
+
source: "continue",
|
|
42
|
+
timestamp,
|
|
43
|
+
session_file: file,
|
|
44
|
+
text,
|
|
45
|
+
});
|
|
46
|
+
addTotals(tokenTotals, usageFromObject(row) || usageFromObject(row.message));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
...(emptyReport("continue", sessionsRoot)),
|
|
52
|
+
files_scanned: files.length,
|
|
53
|
+
prompt_count: prompts.length,
|
|
54
|
+
token_totals: tokenTotals,
|
|
55
|
+
prompts,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function extractContinueUserText(row) {
|
|
60
|
+
if (!row || typeof row !== "object") return "";
|
|
61
|
+
const message = row.message && typeof row.message === "object" ? row.message : row;
|
|
62
|
+
const role = String(message.role || row.role || "").toLowerCase();
|
|
63
|
+
if (role && role !== "user") return "";
|
|
64
|
+
return cleanText(message.content ?? row.content ?? row.text ?? "");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = { inspectContinue, extractContinueUserText };
|
|
@@ -0,0 +1,170 @@
|
|
|
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
|
+
resolveVscodeGlobalStorage,
|
|
15
|
+
} = require("./common");
|
|
16
|
+
|
|
17
|
+
const COPILOT_EXTENSION_IDS = [
|
|
18
|
+
"github.copilot-chat",
|
|
19
|
+
"GitHub.copilot-chat",
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
async function inspectCopilot({ root, range, home = os.homedir(), platform, env } = {}) {
|
|
23
|
+
const roots = root
|
|
24
|
+
? [root]
|
|
25
|
+
: [
|
|
26
|
+
...["Code", "Code - Insiders", "Cursor"].flatMap((appName) =>
|
|
27
|
+
COPILOT_EXTENSION_IDS.map((id) =>
|
|
28
|
+
resolveVscodeGlobalStorage({ home, platform, env, appName, parts: [id] }),
|
|
29
|
+
),
|
|
30
|
+
),
|
|
31
|
+
path.join(home, ".copilot"),
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
const prompts = [];
|
|
35
|
+
const tokenTotals = emptyTotals();
|
|
36
|
+
let filesScanned = 0;
|
|
37
|
+
let usedRoot = roots[0] || null;
|
|
38
|
+
const notes = [];
|
|
39
|
+
|
|
40
|
+
for (const storeRoot of roots) {
|
|
41
|
+
const files = await walkFiles(
|
|
42
|
+
storeRoot,
|
|
43
|
+
(_filePath, name) =>
|
|
44
|
+
name.endsWith(".json") ||
|
|
45
|
+
name.endsWith(".jsonl") ||
|
|
46
|
+
name === "chat-sessions.json" ||
|
|
47
|
+
name.includes("chat"),
|
|
48
|
+
);
|
|
49
|
+
if (!files.length) continue;
|
|
50
|
+
usedRoot = storeRoot;
|
|
51
|
+
|
|
52
|
+
for (const file of files) {
|
|
53
|
+
const data = await readJsonFile(file);
|
|
54
|
+
if (!data) continue;
|
|
55
|
+
filesScanned += 1;
|
|
56
|
+
for (const entry of extractCopilotEntries(data, file)) {
|
|
57
|
+
if (!isInRange(entry.timestamp, range)) continue;
|
|
58
|
+
prompts.push({
|
|
59
|
+
source: "copilot",
|
|
60
|
+
timestamp: entry.timestamp,
|
|
61
|
+
session_file: file,
|
|
62
|
+
text: entry.text,
|
|
63
|
+
});
|
|
64
|
+
addTotals(tokenTotals, entry.tokens);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (filesScanned > 0) break;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (!filesScanned) {
|
|
71
|
+
notes.push(
|
|
72
|
+
"No readable GitHub Copilot Chat session JSON found under VS Code/Cursor globalStorage.",
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
...(emptyReport("copilot", usedRoot, notes)),
|
|
78
|
+
files_scanned: filesScanned,
|
|
79
|
+
prompt_count: prompts.length,
|
|
80
|
+
token_totals: tokenTotals,
|
|
81
|
+
prompts,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function extractCopilotEntries(data, file) {
|
|
86
|
+
const out = [];
|
|
87
|
+
const sessions = Array.isArray(data)
|
|
88
|
+
? data
|
|
89
|
+
: Array.isArray(data.sessions)
|
|
90
|
+
? data.sessions
|
|
91
|
+
: Array.isArray(data.chatSessions)
|
|
92
|
+
? data.chatSessions
|
|
93
|
+
: [data];
|
|
94
|
+
|
|
95
|
+
for (const session of sessions) {
|
|
96
|
+
const requests = Array.isArray(session.requests)
|
|
97
|
+
? session.requests
|
|
98
|
+
: Array.isArray(session.messages)
|
|
99
|
+
? session.messages
|
|
100
|
+
: Array.isArray(session.history)
|
|
101
|
+
? session.history
|
|
102
|
+
: [];
|
|
103
|
+
|
|
104
|
+
for (const req of requests) {
|
|
105
|
+
const text = extractCopilotUserText(req);
|
|
106
|
+
if (!text) continue;
|
|
107
|
+
out.push({
|
|
108
|
+
text,
|
|
109
|
+
timestamp: toIsoTimestamp(
|
|
110
|
+
req.timestamp ||
|
|
111
|
+
req.time ||
|
|
112
|
+
req.createdAt ||
|
|
113
|
+
session.creationDate ||
|
|
114
|
+
session.lastMessageDate ||
|
|
115
|
+
session.createdAt,
|
|
116
|
+
),
|
|
117
|
+
tokens: usageFromObject(req) || usageFromObject(req.response),
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Some exports store the user message on the request itself.
|
|
122
|
+
if (!requests.length) {
|
|
123
|
+
const text = extractCopilotUserText(session);
|
|
124
|
+
if (text) {
|
|
125
|
+
out.push({
|
|
126
|
+
text,
|
|
127
|
+
timestamp: toIsoTimestamp(session.creationDate || session.createdAt || session.timestamp),
|
|
128
|
+
tokens: usageFromObject(session),
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Flat message list files
|
|
135
|
+
if (!out.length && data && typeof data === "object" && Array.isArray(data.messages)) {
|
|
136
|
+
for (const msg of data.messages) {
|
|
137
|
+
const text = extractCopilotUserText(msg);
|
|
138
|
+
if (!text) continue;
|
|
139
|
+
out.push({
|
|
140
|
+
text,
|
|
141
|
+
timestamp: toIsoTimestamp(msg.timestamp || msg.createdAt),
|
|
142
|
+
tokens: usageFromObject(msg),
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
void file;
|
|
148
|
+
return out;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function extractCopilotUserText(row) {
|
|
152
|
+
if (!row || typeof row !== "object") return "";
|
|
153
|
+
const role = String(row.role || row.kind || row.author || row.participant || "").toLowerCase();
|
|
154
|
+
if (role && !/(user|human|request)/.test(role) && role !== "0") {
|
|
155
|
+
// Copilot chat requests often omit role and use `message` / `prompt`.
|
|
156
|
+
if (!row.message && !row.prompt && !row.text && !row.request) return "";
|
|
157
|
+
if (/(assistant|bot|copilot|response|model)/.test(role)) return "";
|
|
158
|
+
}
|
|
159
|
+
return cleanText(
|
|
160
|
+
row.message ??
|
|
161
|
+
row.prompt ??
|
|
162
|
+
row.text ??
|
|
163
|
+
row.request ??
|
|
164
|
+
row.content ??
|
|
165
|
+
row.userMessage ??
|
|
166
|
+
"",
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
module.exports = { inspectCopilot, extractCopilotUserText, extractCopilotEntries };
|