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,368 @@
|
|
|
1
|
+
const CATEGORY_ORDER = [
|
|
2
|
+
"planning",
|
|
3
|
+
"debugging",
|
|
4
|
+
"implementation",
|
|
5
|
+
"refactor",
|
|
6
|
+
"testing",
|
|
7
|
+
"packaging",
|
|
8
|
+
"explanation",
|
|
9
|
+
"research",
|
|
10
|
+
"ui_design",
|
|
11
|
+
"workflow",
|
|
12
|
+
"reference",
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
function analyzePrompts(prompts, { usefulLimit = 60 } = {}) {
|
|
16
|
+
const categories = Object.fromEntries(
|
|
17
|
+
CATEGORY_ORDER.map((name) => [name, { count: 0, examples: [] }]),
|
|
18
|
+
);
|
|
19
|
+
const usefulPrompts = [];
|
|
20
|
+
const usefulForStats = [];
|
|
21
|
+
const referencePrompts = [];
|
|
22
|
+
let usefulCount = 0;
|
|
23
|
+
let usefulCharacterCount = 0;
|
|
24
|
+
let longPromptCount = 0;
|
|
25
|
+
|
|
26
|
+
for (const prompt of prompts || []) {
|
|
27
|
+
const text = String(prompt?.text || "").trim();
|
|
28
|
+
if (!text) continue;
|
|
29
|
+
|
|
30
|
+
const classification = classifyPrompt(text);
|
|
31
|
+
const entry = {
|
|
32
|
+
source: prompt.source || "unknown",
|
|
33
|
+
timestamp: prompt.timestamp || null,
|
|
34
|
+
text,
|
|
35
|
+
category: classification.category,
|
|
36
|
+
categories: classification.categories,
|
|
37
|
+
usefulness: classification.usefulness,
|
|
38
|
+
reasons: classification.reasons,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
if (classification.useful) {
|
|
42
|
+
usefulCount += 1;
|
|
43
|
+
usefulCharacterCount += text.length;
|
|
44
|
+
if (text.length >= 600) longPromptCount += 1;
|
|
45
|
+
const weight = 1 / classification.categories.length;
|
|
46
|
+
for (const category of classification.categories) {
|
|
47
|
+
const bucket = categories[category];
|
|
48
|
+
bucket.count = Number((bucket.count + weight).toFixed(4));
|
|
49
|
+
if (bucket.examples.length < 3) bucket.examples.push(preview(entry));
|
|
50
|
+
}
|
|
51
|
+
usefulForStats.push({
|
|
52
|
+
text: entry.text,
|
|
53
|
+
source: entry.source,
|
|
54
|
+
timestamp: entry.timestamp,
|
|
55
|
+
categories: entry.categories,
|
|
56
|
+
});
|
|
57
|
+
if (usefulPrompts.length < usefulLimit) usefulPrompts.push(entry);
|
|
58
|
+
} else {
|
|
59
|
+
categories.reference.count += 1;
|
|
60
|
+
if (categories.reference.examples.length < 3) categories.reference.examples.push(preview(entry));
|
|
61
|
+
referencePrompts.push(entry);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
total_prompts: (prompts || []).length,
|
|
67
|
+
useful_prompt_count: usefulCount,
|
|
68
|
+
reference_prompt_count: referencePrompts.length,
|
|
69
|
+
useful_ratio:
|
|
70
|
+
prompts && prompts.length > 0 ? Number((usefulCount / prompts.length).toFixed(4)) : 0,
|
|
71
|
+
average_useful_prompt_chars:
|
|
72
|
+
usefulCount > 0 ? Math.round(usefulCharacterCount / usefulCount) : 0,
|
|
73
|
+
long_prompt_ratio:
|
|
74
|
+
usefulCount > 0 ? Number((longPromptCount / usefulCount).toFixed(4)) : 0,
|
|
75
|
+
categories,
|
|
76
|
+
useful_prompts: usefulPrompts,
|
|
77
|
+
useful_for_stats: usefulForStats,
|
|
78
|
+
reference_summary: summarizeReferences(referencePrompts),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function classifyPrompt(text) {
|
|
83
|
+
const normalized = text.toLowerCase();
|
|
84
|
+
const codeScore = codeLikeScore(text);
|
|
85
|
+
const logScore = logLikeScore(text);
|
|
86
|
+
|
|
87
|
+
if (isSystemOrToolNoise(text)) {
|
|
88
|
+
return {
|
|
89
|
+
useful: false,
|
|
90
|
+
usefulness: "reference",
|
|
91
|
+
category: "reference",
|
|
92
|
+
categories: ["reference"],
|
|
93
|
+
reasons: ["system_or_tool_noise"],
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (logScore >= 3) {
|
|
98
|
+
return {
|
|
99
|
+
useful: false,
|
|
100
|
+
usefulness: "reference",
|
|
101
|
+
category: "reference",
|
|
102
|
+
categories: ["reference"],
|
|
103
|
+
reasons: ["looks_like_error_or_log"],
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Heavy pasted files / dumps — never treat as useful intent prompts.
|
|
108
|
+
if (codeScore >= 4) {
|
|
109
|
+
return {
|
|
110
|
+
useful: false,
|
|
111
|
+
usefulness: "reference",
|
|
112
|
+
category: "reference",
|
|
113
|
+
categories: ["reference"],
|
|
114
|
+
reasons: ["looks_like_pasted_code"],
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Milder code blocks without enough natural-language intent.
|
|
119
|
+
if (codeScore >= 3 && !hasSubstantialUserIntent(text)) {
|
|
120
|
+
return {
|
|
121
|
+
useful: false,
|
|
122
|
+
usefulness: "reference",
|
|
123
|
+
category: "reference",
|
|
124
|
+
reasons: ["looks_like_pasted_code"],
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const categories = inferCategories(normalized);
|
|
129
|
+
return {
|
|
130
|
+
useful: true,
|
|
131
|
+
usefulness: "high",
|
|
132
|
+
category: categories[0],
|
|
133
|
+
categories,
|
|
134
|
+
reasons: ["contains_user_intent"],
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function isSystemOrToolNoise(text) {
|
|
139
|
+
const raw = String(text || "");
|
|
140
|
+
if (/<system_notification>/i.test(raw)) return true;
|
|
141
|
+
if (/^\s*<(?:system|tool_result|tool_call|agent_transcript)[\s>]/i.test(raw)) return true;
|
|
142
|
+
if (/\bThe following task has finished\b/i.test(raw)) return true;
|
|
143
|
+
if (/^\s*\[?(system|tool)\]?\s*:/i.test(raw)) return true;
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Intent outside fenced/code lines — avoids counting "IMPLEMENT" inside dumps. */
|
|
148
|
+
function hasSubstantialUserIntent(text) {
|
|
149
|
+
const stripped = String(text || "")
|
|
150
|
+
.replace(/```[\s\S]*?```/g, " ")
|
|
151
|
+
.replace(/^\s*(import|from|const|let|var|def|class|function|export|package|#include)\b.*$/gim, " ")
|
|
152
|
+
.replace(/[{};=<>]{2,}/g, " ")
|
|
153
|
+
.replace(/\s+/g, " ")
|
|
154
|
+
.trim();
|
|
155
|
+
if (stripped.length < 12) return false;
|
|
156
|
+
return hasIntentVerb(stripped.toLowerCase()) || /[\p{Script=Han}]{2,}/u.test(stripped);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function inferCategories(text) {
|
|
160
|
+
const rules = [
|
|
161
|
+
["planning", /(方案|计划|架构|先不要写代码|brainstorm|plan|architecture)/i],
|
|
162
|
+
["debugging", /(bug|报错|错误|失败|修复|debug|error|exception|traceback|syntaxerror)/i],
|
|
163
|
+
["testing", /(测试|test|spec|回归|覆盖率|断言)/i],
|
|
164
|
+
["refactor", /(重构|refactor|抽象|拆分|整理)/i],
|
|
165
|
+
["packaging", /(打包|发布|npm|xpi|release|publish|build)/i],
|
|
166
|
+
["explanation", /(解释|为什么|讲一下|说明|explain|why)/i],
|
|
167
|
+
["research", /(查找|调研|搜索|资料|文档|research|search|look up)/i],
|
|
168
|
+
["ui_design", /(页面|布局|组件|交互|视觉|按钮|ui|ux|style|css|design)/i],
|
|
169
|
+
["workflow", /(workflow|流程|自动化|hook|mcp|skill|agent|instructions|system prompt)/i],
|
|
170
|
+
];
|
|
171
|
+
const matched = rules.filter(([, pattern]) => pattern.test(text)).map(([name]) => name);
|
|
172
|
+
if (/(实现|编写|创建|新增|修改|开发|implement|create|add|write|code|update|build)/i.test(text)) {
|
|
173
|
+
matched.push("implementation");
|
|
174
|
+
}
|
|
175
|
+
return [...new Set(matched.length > 0 ? matched : ["implementation"])];
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function hasIntentVerb(text) {
|
|
179
|
+
return /(帮我|请|我要|我想|实现|做|改|修|优化|设计|解释|分析|生成|add|fix|build|create|implement|explain|analyze|update|refactor)/i.test(
|
|
180
|
+
text,
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function codeLikeScore(text) {
|
|
185
|
+
let score = 0;
|
|
186
|
+
const raw = String(text || "");
|
|
187
|
+
if (/```/.test(raw)) score += 2;
|
|
188
|
+
if (/\b(const|let|var|function|class|return|import|export|async|await|def|self\.|torch\.|nn\.)\b/.test(raw)) {
|
|
189
|
+
score += 1;
|
|
190
|
+
}
|
|
191
|
+
if (/[{};]{2,}/.test(raw)) score += 1;
|
|
192
|
+
if (/=>|<\/?[a-z][\s\S]*>/i.test(raw)) score += 1;
|
|
193
|
+
const lines = raw.split(/\r?\n/);
|
|
194
|
+
if (lines.length >= 8) score += 1;
|
|
195
|
+
// Collapsed single-line dumps still look like code by density.
|
|
196
|
+
if (lines.length < 8 && raw.length > 400) {
|
|
197
|
+
const codeTokens = (raw.match(/\b(def|class|import|return|const|self|torch|nn|plt)\b/gi) || []).length;
|
|
198
|
+
if (codeTokens >= 6) score += 2;
|
|
199
|
+
}
|
|
200
|
+
if (/[A-Za-z0-9_$]+\([^)]*\)/.test(raw)) score += 1;
|
|
201
|
+
return score;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function logLikeScore(text) {
|
|
205
|
+
let score = 0;
|
|
206
|
+
if (/(syntaxerror|typeerror|referenceerror|traceback|exception|stack trace)/i.test(text)) score += 2;
|
|
207
|
+
if (/\bat .+:\d+:\d+/.test(text)) score += 1;
|
|
208
|
+
if (/(warning|error|failed|missing resource|deprecated)/i.test(text)) score += 1;
|
|
209
|
+
if (text.split(/\r?\n/).length >= 6 && /:\d+:\d+/.test(text)) score += 1;
|
|
210
|
+
return score;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function preview(entry) {
|
|
214
|
+
return {
|
|
215
|
+
source: entry.source,
|
|
216
|
+
timestamp: entry.timestamp,
|
|
217
|
+
text: entry.text.slice(0, 180),
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function summarizeReferences(referencePrompts) {
|
|
222
|
+
const signals = emptyReferenceSignals();
|
|
223
|
+
for (const prompt of referencePrompts) {
|
|
224
|
+
addReferenceSignals(signals, prompt.text);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
count: referencePrompts.length,
|
|
229
|
+
code_or_log_count: referencePrompts.length,
|
|
230
|
+
signals: finalizeReferenceSignals(signals),
|
|
231
|
+
examples: referencePrompts.slice(0, 5).map(preview),
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function emptyReferenceSignals() {
|
|
236
|
+
return {
|
|
237
|
+
languages: new Map(),
|
|
238
|
+
file_extensions: new Map(),
|
|
239
|
+
error_types: new Map(),
|
|
240
|
+
files: new Map(),
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function addReferenceSignals(signals, text) {
|
|
245
|
+
const promptLanguages = new Set();
|
|
246
|
+
for (const language of extractFencedLanguages(text)) {
|
|
247
|
+
promptLanguages.add(normalizeLanguage(language));
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
for (const filePath of extractFilePaths(text)) {
|
|
251
|
+
const ext = fileExtension(filePath);
|
|
252
|
+
if (ext && !languageFromExtension(ext)) continue;
|
|
253
|
+
signals.files.set(filePath, { path: filePath, extension: ext || null });
|
|
254
|
+
if (ext) {
|
|
255
|
+
inc(signals.file_extensions, ext);
|
|
256
|
+
const language = languageFromExtension(ext);
|
|
257
|
+
if (language) promptLanguages.add(language);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
for (const language of promptLanguages) inc(signals.languages, language);
|
|
262
|
+
|
|
263
|
+
for (const errorType of extractErrorTypes(text)) {
|
|
264
|
+
inc(signals.error_types, errorType);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function extractFencedLanguages(text) {
|
|
269
|
+
const out = [];
|
|
270
|
+
const re = /```([A-Za-z0-9_+.-]+)/g;
|
|
271
|
+
let match;
|
|
272
|
+
while ((match = re.exec(text))) out.push(match[1]);
|
|
273
|
+
return out;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function extractFilePaths(text) {
|
|
277
|
+
const out = new Set();
|
|
278
|
+
const re =
|
|
279
|
+
/(?:^|\s|["'`(])((?:\.{1,2}\/|\/|[A-Za-z0-9_-]+\/)?[A-Za-z0-9_@./-]+\.(?:[A-Za-z0-9]{1,8}))(?:[:\s)"'`]|$)/g;
|
|
280
|
+
let match;
|
|
281
|
+
while ((match = re.exec(text))) {
|
|
282
|
+
const candidate = match[1].replace(/[),.;]+$/g, "");
|
|
283
|
+
if (candidate.includes("://")) continue;
|
|
284
|
+
if (/^\d+\.\d+$/.test(candidate)) continue;
|
|
285
|
+
out.add(candidate);
|
|
286
|
+
}
|
|
287
|
+
return Array.from(out).slice(0, 80);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function extractErrorTypes(text) {
|
|
291
|
+
const out = [];
|
|
292
|
+
const re =
|
|
293
|
+
/\b(SyntaxError|TypeError|ReferenceError|RangeError|ImportError|ModuleNotFoundError|JSONDecodeError|AssertionError|Error|Exception|Traceback)\b/g;
|
|
294
|
+
let match;
|
|
295
|
+
while ((match = re.exec(text))) out.push(match[1]);
|
|
296
|
+
return out;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function fileExtension(filePath) {
|
|
300
|
+
const match = String(filePath).match(/(\.[A-Za-z0-9]+)$/);
|
|
301
|
+
return match ? match[1].toLowerCase() : "";
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function languageFromExtension(ext) {
|
|
305
|
+
return (
|
|
306
|
+
{
|
|
307
|
+
".js": "javascript",
|
|
308
|
+
".jsx": "jsx",
|
|
309
|
+
".ts": "typescript",
|
|
310
|
+
".tsx": "tsx",
|
|
311
|
+
".py": "python",
|
|
312
|
+
".rb": "ruby",
|
|
313
|
+
".go": "go",
|
|
314
|
+
".rs": "rust",
|
|
315
|
+
".java": "java",
|
|
316
|
+
".kt": "kotlin",
|
|
317
|
+
".swift": "swift",
|
|
318
|
+
".css": "css",
|
|
319
|
+
".scss": "scss",
|
|
320
|
+
".html": "html",
|
|
321
|
+
".json": "json",
|
|
322
|
+
".toml": "toml",
|
|
323
|
+
".yaml": "yaml",
|
|
324
|
+
".yml": "yaml",
|
|
325
|
+
".md": "markdown",
|
|
326
|
+
".sql": "sql",
|
|
327
|
+
".sh": "shell",
|
|
328
|
+
".ftl": "fluent",
|
|
329
|
+
}[ext] || null
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function normalizeLanguage(language) {
|
|
334
|
+
const raw = String(language || "").toLowerCase();
|
|
335
|
+
return (
|
|
336
|
+
{
|
|
337
|
+
js: "javascript",
|
|
338
|
+
mjs: "javascript",
|
|
339
|
+
cjs: "javascript",
|
|
340
|
+
ts: "typescript",
|
|
341
|
+
py: "python",
|
|
342
|
+
sh: "shell",
|
|
343
|
+
bash: "shell",
|
|
344
|
+
}[raw] || raw
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function inc(map, key) {
|
|
349
|
+
if (!key) return;
|
|
350
|
+
map.set(key, (map.get(key) || 0) + 1);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function finalizeReferenceSignals(signals) {
|
|
354
|
+
return {
|
|
355
|
+
languages: mapToObject(signals.languages),
|
|
356
|
+
file_extensions: mapToObject(signals.file_extensions),
|
|
357
|
+
error_types: mapToObject(signals.error_types),
|
|
358
|
+
files: Array.from(signals.files.values()).slice(0, 40),
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function mapToObject(map) {
|
|
363
|
+
return Object.fromEntries(
|
|
364
|
+
Array.from(map.entries()).sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])),
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
module.exports = { analyzePrompts, classifyPrompt, inferCategories };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
function textFromContent(content) {
|
|
2
|
+
if (typeof content === "string") return content.trim();
|
|
3
|
+
if (!Array.isArray(content)) return "";
|
|
4
|
+
|
|
5
|
+
return content
|
|
6
|
+
.map((block) => {
|
|
7
|
+
if (!block || typeof block !== "object") return "";
|
|
8
|
+
const type = String(block.type || "").toLowerCase();
|
|
9
|
+
// Only keep human-readable text blocks — skip tool/system payloads.
|
|
10
|
+
if (type && !/^(text|input_text|output_text)$/.test(type)) return "";
|
|
11
|
+
if (typeof block.text === "string") return block.text;
|
|
12
|
+
if (typeof block.content === "string" && (!type || type === "text")) return block.content;
|
|
13
|
+
return "";
|
|
14
|
+
})
|
|
15
|
+
.filter(Boolean)
|
|
16
|
+
.join("\n")
|
|
17
|
+
.trim();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Trim and collapse horizontal whitespace; keep newlines for code/log scoring. */
|
|
21
|
+
function normalizeWhitespace(text) {
|
|
22
|
+
return String(text || "")
|
|
23
|
+
.replace(/\r\n/g, "\n")
|
|
24
|
+
.replace(/[ \t]+\n/g, "\n")
|
|
25
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
26
|
+
.replace(/[ \t]{2,}/g, " ")
|
|
27
|
+
.trim();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
module.exports = { textFromContent, normalizeWhitespace };
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
const fs = require("node:fs/promises");
|
|
2
|
+
const path = require("node:path");
|
|
3
|
+
const os = require("node:os");
|
|
4
|
+
|
|
5
|
+
const HOOK_SCRIPT = path.join(__dirname, "..", "..", "bin", "hook.js");
|
|
6
|
+
|
|
7
|
+
async function install({ home = os.homedir() } = {}) {
|
|
8
|
+
const results = {};
|
|
9
|
+
|
|
10
|
+
// --- Claude Code ---
|
|
11
|
+
const claudeSettingsPath = path.join(home, ".claude", "settings.json");
|
|
12
|
+
try {
|
|
13
|
+
const result = await installClaudeHook(claudeSettingsPath);
|
|
14
|
+
results.claude = result;
|
|
15
|
+
} catch (err) {
|
|
16
|
+
results.claude = { installed: false, error: err.message };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// --- Codex ---
|
|
20
|
+
const codexConfigPath = path.join(home, ".codex", "config.toml");
|
|
21
|
+
try {
|
|
22
|
+
const result = await installCodexHook(codexConfigPath);
|
|
23
|
+
results.codex = result;
|
|
24
|
+
} catch (err) {
|
|
25
|
+
results.codex = { installed: false, error: err.message };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return results;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function uninstall({ home = os.homedir() } = {}) {
|
|
32
|
+
const results = {};
|
|
33
|
+
|
|
34
|
+
const claudeSettingsPath = path.join(home, ".claude", "settings.json");
|
|
35
|
+
try {
|
|
36
|
+
const result = await removeClaudeHook(claudeSettingsPath);
|
|
37
|
+
results.claude = result;
|
|
38
|
+
} catch (err) {
|
|
39
|
+
results.claude = { removed: false, error: err.message };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const codexConfigPath = path.join(home, ".codex", "config.toml");
|
|
43
|
+
try {
|
|
44
|
+
const result = await removeCodexHook(codexConfigPath);
|
|
45
|
+
results.codex = result;
|
|
46
|
+
} catch (err) {
|
|
47
|
+
results.codex = { removed: false, error: err.message };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return results;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/* ── Claude Code ─────────────────────────────────────── */
|
|
54
|
+
|
|
55
|
+
async function readJson(filePath) {
|
|
56
|
+
try {
|
|
57
|
+
const raw = await fs.readFile(filePath, "utf8");
|
|
58
|
+
return JSON.parse(raw);
|
|
59
|
+
} catch {
|
|
60
|
+
return {};
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function installClaudeHook(settingsPath) {
|
|
65
|
+
const cmd = `/usr/bin/env node ${HOOK_SCRIPT} --source=claude`;
|
|
66
|
+
const settings = await readJson(settingsPath);
|
|
67
|
+
|
|
68
|
+
const hooks = settings.hooks || {};
|
|
69
|
+
const sessionEnd = Array.isArray(hooks.SessionEnd) ? [...hooks.SessionEnd] : [];
|
|
70
|
+
|
|
71
|
+
// Check if already installed
|
|
72
|
+
for (const entry of sessionEnd) {
|
|
73
|
+
const subHooks = entry?.hooks || (entry?.command ? [entry] : []);
|
|
74
|
+
for (const h of subHooks) {
|
|
75
|
+
if (h?.command === cmd) {
|
|
76
|
+
return { installed: false, reason: "already-configured" };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Add hook
|
|
82
|
+
sessionEnd.push({ hooks: [{ type: "command", command: cmd }] });
|
|
83
|
+
|
|
84
|
+
const newSettings = { ...settings, hooks: { ...hooks, SessionEnd: sessionEnd } };
|
|
85
|
+
|
|
86
|
+
// Backup
|
|
87
|
+
try {
|
|
88
|
+
await fs.mkdir(path.dirname(settingsPath), { recursive: true });
|
|
89
|
+
await fs.writeFile(settingsPath + ".vibe-backup", JSON.stringify(settings, null, 2));
|
|
90
|
+
} catch {}
|
|
91
|
+
|
|
92
|
+
await fs.writeFile(settingsPath, JSON.stringify(newSettings, null, 2));
|
|
93
|
+
return { installed: true, path: settingsPath };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function removeClaudeHook(settingsPath) {
|
|
97
|
+
const cmd = `/usr/bin/env node ${HOOK_SCRIPT} --source=claude`;
|
|
98
|
+
const settings = await readJson(settingsPath);
|
|
99
|
+
if (!settings.hooks?.SessionEnd) return { removed: false, reason: "no-hooks" };
|
|
100
|
+
|
|
101
|
+
const sessionEnd = settings.hooks.SessionEnd.filter((entry) => {
|
|
102
|
+
const subHooks = entry?.hooks || (entry?.command ? [entry] : []);
|
|
103
|
+
return !subHooks.some((h) => h?.command === cmd);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
if (sessionEnd.length === settings.hooks.SessionEnd.length) {
|
|
107
|
+
return { removed: false, reason: "hook-not-found" };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const newHooks = { ...settings.hooks, SessionEnd: sessionEnd };
|
|
111
|
+
if (sessionEnd.length === 0) delete newHooks.SessionEnd;
|
|
112
|
+
const newSettings = { ...settings, hooks: newHooks };
|
|
113
|
+
if (Object.keys(newSettings.hooks).length === 0) delete newSettings.hooks;
|
|
114
|
+
|
|
115
|
+
await fs.writeFile(settingsPath, JSON.stringify(newSettings, null, 2));
|
|
116
|
+
return { removed: true, path: settingsPath };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/* ── Codex ─────────────────────────────────────────────── */
|
|
120
|
+
|
|
121
|
+
const CODEX_HOOK_TOML = `
|
|
122
|
+
# Vibe Wrapper — token tracking hook
|
|
123
|
+
[[notify.hooks]]
|
|
124
|
+
event = "session_end"
|
|
125
|
+
command = "node ${HOOK_SCRIPT}"
|
|
126
|
+
`.trim() + "\n";
|
|
127
|
+
|
|
128
|
+
async function installCodexHook(configPath) {
|
|
129
|
+
try {
|
|
130
|
+
const existing = await fs.readFile(configPath, "utf8");
|
|
131
|
+
if (existing.includes(HOOK_SCRIPT)) {
|
|
132
|
+
return { installed: false, reason: "already-configured" };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Backup
|
|
136
|
+
try { await fs.writeFile(configPath + ".vibe-backup", existing); } catch {}
|
|
137
|
+
|
|
138
|
+
await fs.appendFile(configPath, "\n" + CODEX_HOOK_TOML.replace("${HOOK_SCRIPT}", HOOK_SCRIPT));
|
|
139
|
+
return { installed: true, path: configPath };
|
|
140
|
+
} catch {
|
|
141
|
+
// No config.toml — create minimal one
|
|
142
|
+
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
|
143
|
+
await fs.writeFile(configPath, CODEX_HOOK_TOML.replace("${HOOK_SCRIPT}", HOOK_SCRIPT));
|
|
144
|
+
return { installed: true, path: configPath, reason: "created-new-config" };
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function removeCodexHook(configPath) {
|
|
149
|
+
try {
|
|
150
|
+
const existing = await fs.readFile(configPath, "utf8");
|
|
151
|
+
if (!existing.includes(HOOK_SCRIPT)) {
|
|
152
|
+
return { removed: false, reason: "hook-not-found" };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const lines = existing.split("\n").filter((line) => !line.includes(HOOK_SCRIPT));
|
|
156
|
+
// Also remove the Vibe Wrapper comment and empty [[notify.hooks]] blocks
|
|
157
|
+
const cleaned = cleanCodexConfig(lines);
|
|
158
|
+
|
|
159
|
+
await fs.writeFile(configPath, cleaned);
|
|
160
|
+
return { removed: true, path: configPath };
|
|
161
|
+
} catch {
|
|
162
|
+
return { removed: false, reason: "config-missing" };
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function cleanCodexConfig(lines) {
|
|
167
|
+
const out = [];
|
|
168
|
+
let skip = false;
|
|
169
|
+
for (let i = 0; i < lines.length; i++) {
|
|
170
|
+
const line = lines[i];
|
|
171
|
+
// Skip Vibe Wrapper comment and associated hook entries
|
|
172
|
+
if (line.includes("# Vibe Wrapper")) {
|
|
173
|
+
skip = true;
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
if (skip && (line.trim().startsWith("[") || line.trim() === "")) {
|
|
177
|
+
skip = false;
|
|
178
|
+
}
|
|
179
|
+
if (!skip) out.push(line);
|
|
180
|
+
}
|
|
181
|
+
return out.join("\n").trim() + "\n";
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
module.exports = { install, uninstall };
|