vibeglish 0.1.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/CLAUDE.md +22 -0
- package/README.md +82 -0
- package/package.json +23 -0
- package/src/achievements.mjs +240 -0
- package/src/cli/commands/export.mjs +71 -0
- package/src/cli/commands/hook-test.mjs +60 -0
- package/src/cli/commands/init.mjs +155 -0
- package/src/cli/commands/report.mjs +141 -0
- package/src/cli/commands/review.mjs +90 -0
- package/src/cli/commands/serve.mjs +102 -0
- package/src/cli/commands/status.mjs +53 -0
- package/src/cli/commands/uninstall.mjs +43 -0
- package/src/cli/index.mjs +63 -0
- package/src/constants.mjs +52 -0
- package/src/dashboard/index.html +710 -0
- package/src/hooks/capture.sh +111 -0
- package/src/review/engine.mjs +185 -0
- package/src/utils.mjs +64 -0
- package/test/achievements.test.mjs +116 -0
- package/test/capture.test.mjs +108 -0
- package/test/engine.test.mjs +56 -0
- package/vibeglish-prd.md +591 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { REVIEWED_DIR } from '../../constants.mjs';
|
|
3
|
+
import { readJSON, listFiles, formatDate, parseDate } from '../../utils.mjs';
|
|
4
|
+
import { checkAchievements } from '../../achievements.mjs';
|
|
5
|
+
|
|
6
|
+
export default async function report(args) {
|
|
7
|
+
const daysArg = args.indexOf('--days');
|
|
8
|
+
const days = daysArg !== -1 && args[daysArg + 1] ? parseInt(args[daysArg + 1]) : 7;
|
|
9
|
+
|
|
10
|
+
const today = parseDate(formatDate());
|
|
11
|
+
const startDate = new Date(today);
|
|
12
|
+
startDate.setDate(startDate.getDate() - days + 1);
|
|
13
|
+
|
|
14
|
+
const allFiles = listFiles(REVIEWED_DIR, '.json');
|
|
15
|
+
if (allFiles.length === 0) {
|
|
16
|
+
console.log('No reviewed data yet. Run "vibeglish review" first.');
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Load all reviewed data
|
|
21
|
+
const allData = [];
|
|
22
|
+
const periodData = [];
|
|
23
|
+
const startStr = formatDate(startDate);
|
|
24
|
+
|
|
25
|
+
for (const f of allFiles) {
|
|
26
|
+
const data = readJSON(join(REVIEWED_DIR, f));
|
|
27
|
+
if (!data) continue;
|
|
28
|
+
allData.push(data);
|
|
29
|
+
if (data.date >= startStr) {
|
|
30
|
+
periodData.push(data);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (periodData.length === 0) {
|
|
35
|
+
console.log(`No data in the last ${days} days.`);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Compute stats
|
|
40
|
+
const totalCaptures = periodData.reduce((s, d) => s + d.stats.total_captured, 0);
|
|
41
|
+
const totalReviewed = periodData.reduce((s, d) => s + d.stats.total_reviewed, 0);
|
|
42
|
+
const totalSkipped = periodData.reduce((s, d) => s + d.stats.skipped, 0);
|
|
43
|
+
const avgScore = periodData.reduce((s, d) => s + d.stats.avg_score, 0) / periodData.length;
|
|
44
|
+
|
|
45
|
+
// Previous period for comparison
|
|
46
|
+
const prevStart = new Date(startDate);
|
|
47
|
+
prevStart.setDate(prevStart.getDate() - days);
|
|
48
|
+
const prevStartStr = formatDate(prevStart);
|
|
49
|
+
const prevData = allData.filter(d => d.date >= prevStartStr && d.date < startStr);
|
|
50
|
+
const prevAvg = prevData.length > 0
|
|
51
|
+
? prevData.reduce((s, d) => s + d.stats.avg_score, 0) / prevData.length
|
|
52
|
+
: null;
|
|
53
|
+
const scoreDiff = prevAvg !== null ? avgScore - prevAvg : null;
|
|
54
|
+
|
|
55
|
+
// Issue breakdown
|
|
56
|
+
const issueMap = {};
|
|
57
|
+
for (const day of periodData) {
|
|
58
|
+
for (const entry of day.entries) {
|
|
59
|
+
for (const issue of entry.issues || []) {
|
|
60
|
+
const key = issue.rule || issue.type;
|
|
61
|
+
issueMap[key] = (issueMap[key] || 0) + 1;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const topIssues = Object.entries(issueMap)
|
|
66
|
+
.sort((a, b) => b[1] - a[1])
|
|
67
|
+
.slice(0, 3);
|
|
68
|
+
|
|
69
|
+
// Daily scores
|
|
70
|
+
const dailyScores = periodData
|
|
71
|
+
.sort((a, b) => a.date.localeCompare(b.date))
|
|
72
|
+
.map(d => ({ date: d.date, score: d.stats.avg_score }));
|
|
73
|
+
|
|
74
|
+
// Clean prompts
|
|
75
|
+
const cleanCount = periodData.reduce((s, d) =>
|
|
76
|
+
s + d.entries.filter(e => e.is_clean).length, 0);
|
|
77
|
+
|
|
78
|
+
// Achievements
|
|
79
|
+
const achievements = checkAchievements(allData);
|
|
80
|
+
const recentUnlocked = achievements.filter(a => {
|
|
81
|
+
if (!a.unlockedAt) return false;
|
|
82
|
+
return a.unlockedAt >= startStr;
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// Format dates
|
|
86
|
+
const from = periodData[0].date.slice(5);
|
|
87
|
+
const to = periodData[periodData.length - 1].date.slice(5);
|
|
88
|
+
|
|
89
|
+
// Output
|
|
90
|
+
const cols = process.stdout.columns || 50;
|
|
91
|
+
const barWidth = Math.min(10, Math.floor(cols / 6));
|
|
92
|
+
|
|
93
|
+
console.log(`
|
|
94
|
+
\u{1F4CA} VibeGlish Report (${from} ~ ${to})
|
|
95
|
+
${'━'.repeat(Math.min(cols - 2, 45))}
|
|
96
|
+
|
|
97
|
+
Prompts captured: ${totalCaptures}
|
|
98
|
+
Prompts reviewed: ${totalReviewed} (${totalSkipped} skipped)
|
|
99
|
+
Average score: ${avgScore.toFixed(1)}${scoreDiff !== null ? ` ${scoreDiff >= 0 ? '\u25B2' : '\u25BC'} ${scoreDiff >= 0 ? '+' : ''}${scoreDiff.toFixed(1)} vs prev` : ''}`);
|
|
100
|
+
|
|
101
|
+
if (topIssues.length > 0) {
|
|
102
|
+
console.log(`
|
|
103
|
+
\u{1F525} Top ${topIssues.length} recurring mistakes:
|
|
104
|
+
${'┌' + '─'.repeat(42) + '┐'}`);
|
|
105
|
+
const maxCount = topIssues[0][1];
|
|
106
|
+
for (let i = 0; i < topIssues.length; i++) {
|
|
107
|
+
const [rule, count] = topIssues[i];
|
|
108
|
+
const filled = Math.round((count / maxCount) * barWidth);
|
|
109
|
+
const bar = '\u2588'.repeat(filled) + '\u2591'.repeat(barWidth - filled);
|
|
110
|
+
const label = rule.length > 20 ? rule.slice(0, 20) + '...' : rule.padEnd(23);
|
|
111
|
+
console.log(` │ ${i + 1}. ${label} ${String(count).padStart(3)} 次 ${bar} │`);
|
|
112
|
+
}
|
|
113
|
+
console.log(` ${'└' + '─'.repeat(42) + '┘'}`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
console.log(`
|
|
117
|
+
\u{1F48E} Clean prompts (score >= 95): ${cleanCount} / ${totalReviewed} (${totalReviewed > 0 ? ((cleanCount / totalReviewed) * 100).toFixed(1) : 0}%)`);
|
|
118
|
+
|
|
119
|
+
if (dailyScores.length > 0) {
|
|
120
|
+
console.log(`\n \u{1F4C8} Score trend:`);
|
|
121
|
+
const dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
|
122
|
+
const maxScore = Math.max(...dailyScores.map(d => d.score), 1);
|
|
123
|
+
const bestDay = dailyScores.reduce((best, d) => d.score > best.score ? d : best, dailyScores[0]);
|
|
124
|
+
for (const ds of dailyScores) {
|
|
125
|
+
const dayName = dayNames[parseDate(ds.date).getDay()];
|
|
126
|
+
const filled = Math.round((ds.score / 100) * barWidth);
|
|
127
|
+
const bar = '\u2588'.repeat(filled) + '\u2591'.repeat(barWidth - filled);
|
|
128
|
+
const marker = ds.date === bestDay.date ? ' \u2190 best!' : '';
|
|
129
|
+
console.log(` ${dayName} ${bar} ${ds.score.toFixed(0)}${marker}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (recentUnlocked.length > 0) {
|
|
134
|
+
console.log(`\n \u{1F3C6} Achievements unlocked:`);
|
|
135
|
+
for (const ach of recentUnlocked) {
|
|
136
|
+
console.log(` "${ach.name}" — ${ach.description}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
console.log('');
|
|
141
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { existsSync, copyFileSync } from 'node:fs';
|
|
3
|
+
import { RAW_DIR, REVIEWED_DIR } from '../../constants.mjs';
|
|
4
|
+
import { readJSONL, readJSON, writeJSON, listFiles, formatDate, dateRange, ensureDir } from '../../utils.mjs';
|
|
5
|
+
import { reviewBatch, computeStats } from '../../review/engine.mjs';
|
|
6
|
+
|
|
7
|
+
export default async function review(args) {
|
|
8
|
+
const force = args.includes('--force');
|
|
9
|
+
let dates = [];
|
|
10
|
+
|
|
11
|
+
const dateIdx = args.indexOf('--date');
|
|
12
|
+
const fromIdx = args.indexOf('--from');
|
|
13
|
+
const toIdx = args.indexOf('--to');
|
|
14
|
+
|
|
15
|
+
if (dateIdx !== -1 && args[dateIdx + 1]) {
|
|
16
|
+
dates = [args[dateIdx + 1]];
|
|
17
|
+
} else if (fromIdx !== -1 && toIdx !== -1 && args[fromIdx + 1] && args[toIdx + 1]) {
|
|
18
|
+
dates = dateRange(args[fromIdx + 1], args[toIdx + 1]);
|
|
19
|
+
} else {
|
|
20
|
+
// Default: all unreviewed dates
|
|
21
|
+
const rawFiles = listFiles(RAW_DIR, '.jsonl');
|
|
22
|
+
const reviewedFiles = new Set(listFiles(REVIEWED_DIR, '.json').map(f => f.replace('.json', '')));
|
|
23
|
+
|
|
24
|
+
dates = rawFiles
|
|
25
|
+
.map(f => f.replace('.jsonl', ''))
|
|
26
|
+
.filter(d => force || !reviewedFiles.has(d));
|
|
27
|
+
|
|
28
|
+
if (dates.length === 0) {
|
|
29
|
+
console.log('No pending prompts to review. Capture some prompts first!');
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
console.log(`Reviewing ${dates.length} day(s)...`);
|
|
35
|
+
|
|
36
|
+
for (const date of dates) {
|
|
37
|
+
const rawFile = join(RAW_DIR, `${date}.jsonl`);
|
|
38
|
+
const reviewedFile = join(REVIEWED_DIR, `${date}.json`);
|
|
39
|
+
|
|
40
|
+
if (!existsSync(rawFile)) {
|
|
41
|
+
console.log(` ${date}: no raw data, skipping`);
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (existsSync(reviewedFile) && !force) {
|
|
46
|
+
console.log(` ${date}: already reviewed, skipping (use --force to re-review)`);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Copy raw file to avoid read/write conflict
|
|
51
|
+
const tmpFile = join(RAW_DIR, `${date}.jsonl.tmp`);
|
|
52
|
+
copyFileSync(rawFile, tmpFile);
|
|
53
|
+
|
|
54
|
+
const entries = readJSONL(tmpFile);
|
|
55
|
+
if (entries.length === 0) {
|
|
56
|
+
console.log(` ${date}: empty file, skipping`);
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
console.log(` ${date}: ${entries.length} prompts`);
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
const results = await reviewBatch(entries);
|
|
64
|
+
const stats = computeStats(results);
|
|
65
|
+
|
|
66
|
+
const output = {
|
|
67
|
+
date,
|
|
68
|
+
reviewed_at: new Date().toISOString(),
|
|
69
|
+
model: 'claude-sonnet-4-20250514',
|
|
70
|
+
stats,
|
|
71
|
+
entries: results.filter(e => !e.skipped),
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
ensureDir(REVIEWED_DIR);
|
|
75
|
+
writeJSON(reviewedFile, output);
|
|
76
|
+
|
|
77
|
+
console.log(` ${date}: done (avg score: ${stats.avg_score}, reviewed: ${stats.total_reviewed}, skipped: ${stats.skipped})`);
|
|
78
|
+
} catch (err) {
|
|
79
|
+
console.error(` ${date}: failed — ${err.message}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Clean up temp file
|
|
83
|
+
try {
|
|
84
|
+
const { unlinkSync } = await import('node:fs');
|
|
85
|
+
unlinkSync(tmpFile);
|
|
86
|
+
} catch { /* ignore */ }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
console.log('\nReview complete! Run "vibeglish serve" to view results.');
|
|
90
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { createServer } from 'node:http';
|
|
2
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { REVIEWED_DIR, DASHBOARD_DIR, CONFIG_PATH, DEFAULT_CONFIG } from '../../constants.mjs';
|
|
5
|
+
import { readJSON, listFiles } from '../../utils.mjs';
|
|
6
|
+
import { checkAchievements } from '../../achievements.mjs';
|
|
7
|
+
|
|
8
|
+
export default async function serve(args) {
|
|
9
|
+
const portIdx = args.indexOf('--port');
|
|
10
|
+
const config = readJSON(CONFIG_PATH) || DEFAULT_CONFIG;
|
|
11
|
+
let port = portIdx !== -1 && args[portIdx + 1] ? parseInt(args[portIdx + 1]) : config.dashboard_port || 6188;
|
|
12
|
+
|
|
13
|
+
const dashboardFile = join(DASHBOARD_DIR, 'index.html');
|
|
14
|
+
if (!existsSync(dashboardFile)) {
|
|
15
|
+
console.error('Dashboard not found. Run "vibeglish init" first.');
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const server = createServer((req, res) => {
|
|
20
|
+
const url = new URL(req.url, `http://localhost:${port}`);
|
|
21
|
+
|
|
22
|
+
if (url.pathname === '/api/data') {
|
|
23
|
+
handleDataAPI(url, res);
|
|
24
|
+
} else {
|
|
25
|
+
// Serve dashboard
|
|
26
|
+
try {
|
|
27
|
+
const html = readFileSync(dashboardFile, 'utf-8');
|
|
28
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
29
|
+
res.end(html);
|
|
30
|
+
} catch (err) {
|
|
31
|
+
res.writeHead(500);
|
|
32
|
+
res.end('Failed to load dashboard');
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Try port, increment if busy
|
|
38
|
+
let attempts = 0;
|
|
39
|
+
const tryListen = () => {
|
|
40
|
+
server.listen(port, '127.0.0.1', () => {
|
|
41
|
+
console.log(`VibeGlish Dashboard running at http://localhost:${port}`);
|
|
42
|
+
console.log('Press Ctrl+C to stop.\n');
|
|
43
|
+
|
|
44
|
+
// Try to open browser
|
|
45
|
+
import('node:child_process').then(({ exec }) => {
|
|
46
|
+
const cmd = process.platform === 'darwin' ? 'open' : 'xdg-open';
|
|
47
|
+
exec(`${cmd} http://localhost:${port}`, () => {});
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
server.on('error', (err) => {
|
|
52
|
+
if (err.code === 'EADDRINUSE' && attempts < 10) {
|
|
53
|
+
attempts++;
|
|
54
|
+
port++;
|
|
55
|
+
tryListen();
|
|
56
|
+
} else {
|
|
57
|
+
console.error(`Cannot start server: ${err.message}`);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
tryListen();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function handleDataAPI(url, res) {
|
|
67
|
+
try {
|
|
68
|
+
const dateParam = url.searchParams.get('date');
|
|
69
|
+
const fromParam = url.searchParams.get('from');
|
|
70
|
+
const toParam = url.searchParams.get('to');
|
|
71
|
+
|
|
72
|
+
const files = listFiles(REVIEWED_DIR, '.json');
|
|
73
|
+
let data = [];
|
|
74
|
+
|
|
75
|
+
for (const f of files) {
|
|
76
|
+
const date = f.replace('.json', '');
|
|
77
|
+
if (dateParam && date !== dateParam) continue;
|
|
78
|
+
if (fromParam && date < fromParam) continue;
|
|
79
|
+
if (toParam && date > toParam) continue;
|
|
80
|
+
|
|
81
|
+
const d = readJSON(join(REVIEWED_DIR, f));
|
|
82
|
+
if (d) data.push(d);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
data.sort((a, b) => a.date.localeCompare(b.date));
|
|
86
|
+
|
|
87
|
+
// Check achievements
|
|
88
|
+
const allData = files.map(f => readJSON(join(REVIEWED_DIR, f))).filter(Boolean);
|
|
89
|
+
const achievements = checkAchievements(allData);
|
|
90
|
+
|
|
91
|
+
const result = { days: data, achievements };
|
|
92
|
+
|
|
93
|
+
res.writeHead(200, {
|
|
94
|
+
'Content-Type': 'application/json',
|
|
95
|
+
'Access-Control-Allow-Origin': '*',
|
|
96
|
+
});
|
|
97
|
+
res.end(JSON.stringify(result));
|
|
98
|
+
} catch (err) {
|
|
99
|
+
res.writeHead(500);
|
|
100
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { RAW_DIR, REVIEWED_DIR } from '../../constants.mjs';
|
|
4
|
+
import { listFiles, formatDate } from '../../utils.mjs';
|
|
5
|
+
|
|
6
|
+
export default async function status() {
|
|
7
|
+
const today = formatDate();
|
|
8
|
+
const rawFiles = listFiles(RAW_DIR, '.jsonl');
|
|
9
|
+
const reviewedFiles = listFiles(REVIEWED_DIR, '.json');
|
|
10
|
+
const reviewedDates = new Set(reviewedFiles.map(f => f.replace('.json', '')));
|
|
11
|
+
|
|
12
|
+
// Today's captures
|
|
13
|
+
let todayCount = 0;
|
|
14
|
+
const todayFile = join(RAW_DIR, `${today}.jsonl`);
|
|
15
|
+
try {
|
|
16
|
+
todayCount = readFileSync(todayFile, 'utf-8').trim().split('\n').filter(Boolean).length;
|
|
17
|
+
} catch { /* no file */ }
|
|
18
|
+
|
|
19
|
+
// Total captures
|
|
20
|
+
let totalCaptures = 0;
|
|
21
|
+
for (const f of rawFiles) {
|
|
22
|
+
try {
|
|
23
|
+
totalCaptures += readFileSync(join(RAW_DIR, f), 'utf-8').trim().split('\n').filter(Boolean).length;
|
|
24
|
+
} catch { /* skip */ }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Pending review
|
|
28
|
+
const pendingDates = rawFiles
|
|
29
|
+
.map(f => f.replace('.jsonl', ''))
|
|
30
|
+
.filter(d => !reviewedDates.has(d));
|
|
31
|
+
|
|
32
|
+
// Total reviewed
|
|
33
|
+
let totalReviewed = 0;
|
|
34
|
+
for (const f of reviewedFiles) {
|
|
35
|
+
try {
|
|
36
|
+
const data = JSON.parse(readFileSync(join(REVIEWED_DIR, f), 'utf-8'));
|
|
37
|
+
totalReviewed += data.stats?.total_reviewed || 0;
|
|
38
|
+
} catch { /* skip */ }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
console.log(`
|
|
42
|
+
VibeGlish Status
|
|
43
|
+
────────────────────────────────
|
|
44
|
+
Today (${today}): ${todayCount} prompts captured
|
|
45
|
+
Total captured: ${totalCaptures}
|
|
46
|
+
Total reviewed: ${totalReviewed}
|
|
47
|
+
Pending review: ${pendingDates.length} day(s)
|
|
48
|
+
${pendingDates.length > 0 ? ` ${pendingDates.join(', ')}` : ''}
|
|
49
|
+
────────────────────────────────
|
|
50
|
+
Raw data: ${RAW_DIR}
|
|
51
|
+
Reviewed: ${REVIEWED_DIR}
|
|
52
|
+
`);
|
|
53
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { CLAUDE_SETTINGS_PATH, VIBEGLISH_DIR } from '../../constants.mjs';
|
|
3
|
+
import { readJSON, writeJSON } from '../../utils.mjs';
|
|
4
|
+
|
|
5
|
+
export default async function uninstall() {
|
|
6
|
+
if (!existsSync(CLAUDE_SETTINGS_PATH)) {
|
|
7
|
+
console.log('No Claude settings file found — nothing to uninstall.');
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const settings = readJSON(CLAUDE_SETTINGS_PATH);
|
|
12
|
+
if (!settings?.hooks?.UserPromptSubmit) {
|
|
13
|
+
console.log('No UserPromptSubmit hook found — nothing to uninstall.');
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const before = settings.hooks.UserPromptSubmit.length;
|
|
18
|
+
settings.hooks.UserPromptSubmit = settings.hooks.UserPromptSubmit.filter(entry =>
|
|
19
|
+
!entry.hooks?.some(h => h.command?.includes('vibeglish') || h.command?.includes('capture.sh'))
|
|
20
|
+
);
|
|
21
|
+
const after = settings.hooks.UserPromptSubmit.length;
|
|
22
|
+
|
|
23
|
+
if (before === after) {
|
|
24
|
+
console.log('VibeGlish hook not found in settings — nothing to uninstall.');
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Clean up empty arrays
|
|
29
|
+
if (settings.hooks.UserPromptSubmit.length === 0) {
|
|
30
|
+
delete settings.hooks.UserPromptSubmit;
|
|
31
|
+
}
|
|
32
|
+
if (Object.keys(settings.hooks).length === 0) {
|
|
33
|
+
delete settings.hooks;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
writeJSON(CLAUDE_SETTINGS_PATH, settings);
|
|
37
|
+
|
|
38
|
+
console.log(`VibeGlish hook removed from Claude Code settings.
|
|
39
|
+
|
|
40
|
+
Your data is preserved in: ${VIBEGLISH_DIR}
|
|
41
|
+
To delete all data: rm -rf ${VIBEGLISH_DIR}
|
|
42
|
+
`);
|
|
43
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const [command, ...args] = process.argv.slice(2);
|
|
4
|
+
|
|
5
|
+
const commands = {
|
|
6
|
+
init: () => import('./commands/init.mjs'),
|
|
7
|
+
status: () => import('./commands/status.mjs'),
|
|
8
|
+
review: () => import('./commands/review.mjs'),
|
|
9
|
+
report: () => import('./commands/report.mjs'),
|
|
10
|
+
serve: () => import('./commands/serve.mjs'),
|
|
11
|
+
export: () => import('./commands/export.mjs'),
|
|
12
|
+
'hook-test': () => import('./commands/hook-test.mjs'),
|
|
13
|
+
uninstall: () => import('./commands/uninstall.mjs'),
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
function printHelp() {
|
|
17
|
+
console.log(`
|
|
18
|
+
vibeglish — Learn English naturally while vibe coding
|
|
19
|
+
|
|
20
|
+
Usage: vibeglish <command> [options]
|
|
21
|
+
|
|
22
|
+
Commands:
|
|
23
|
+
init Initialize VibeGlish and install the Claude Code hook
|
|
24
|
+
status Show capture/review statistics
|
|
25
|
+
review Run AI correction on captured prompts
|
|
26
|
+
report Print a weekly summary report in the terminal
|
|
27
|
+
serve Start the local Web Dashboard
|
|
28
|
+
export Export all reviewed data as JSON or CSV
|
|
29
|
+
hook-test Test if the capture hook is working
|
|
30
|
+
uninstall Remove the hook (keeps your data)
|
|
31
|
+
|
|
32
|
+
Options:
|
|
33
|
+
--help, -h Show this help message
|
|
34
|
+
--version Show version
|
|
35
|
+
`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!command || command === '--help' || command === '-h') {
|
|
39
|
+
printHelp();
|
|
40
|
+
process.exit(0);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (command === '--version') {
|
|
44
|
+
const { readFileSync } = await import('node:fs');
|
|
45
|
+
const { fileURLToPath } = await import('node:url');
|
|
46
|
+
const { join, dirname } = await import('node:path');
|
|
47
|
+
const pkg = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), '../../package.json'), 'utf-8'));
|
|
48
|
+
console.log(`vibeglish v${pkg.version}`);
|
|
49
|
+
process.exit(0);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (!commands[command]) {
|
|
53
|
+
console.error(`Unknown command: ${command}\nRun "vibeglish --help" for usage.`);
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
const mod = await commands[command]();
|
|
59
|
+
await mod.default(args);
|
|
60
|
+
} catch (err) {
|
|
61
|
+
console.error(`Error: ${err.message}`);
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { homedir } from 'node:os';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
const HOME = homedir();
|
|
5
|
+
|
|
6
|
+
export const VIBEGLISH_DIR = join(HOME, '.vibeglish');
|
|
7
|
+
export const RAW_DIR = join(VIBEGLISH_DIR, 'raw');
|
|
8
|
+
export const REVIEWED_DIR = join(VIBEGLISH_DIR, 'reviewed');
|
|
9
|
+
export const HOOKS_DIR = join(VIBEGLISH_DIR, 'hooks');
|
|
10
|
+
export const DASHBOARD_DIR = join(VIBEGLISH_DIR, 'dashboard');
|
|
11
|
+
export const CONFIG_PATH = join(VIBEGLISH_DIR, 'config.json');
|
|
12
|
+
export const ERROR_LOG = join(VIBEGLISH_DIR, 'error.log');
|
|
13
|
+
export const ACHIEVEMENTS_PATH = join(VIBEGLISH_DIR, 'achievements.json');
|
|
14
|
+
export const CLAUDE_SETTINGS_PATH = join(HOME, '.claude', 'settings.json');
|
|
15
|
+
|
|
16
|
+
export const DEFAULT_CONFIG = {
|
|
17
|
+
locale: 'zh-CN',
|
|
18
|
+
min_word_count: 4,
|
|
19
|
+
max_code_ratio: 0.7,
|
|
20
|
+
batch_size: 20,
|
|
21
|
+
dashboard_port: 6188,
|
|
22
|
+
auto_review_cron: null,
|
|
23
|
+
score_explanation_language: 'zh-CN',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const REVIEW_SYSTEM_PROMPT = `You are an English writing coach for non-native developers. You will receive
|
|
27
|
+
a batch of prompts that a Chinese developer wrote to an AI coding assistant.
|
|
28
|
+
|
|
29
|
+
For each prompt, provide:
|
|
30
|
+
1. "corrected": The natural, idiomatic English version. Preserve technical
|
|
31
|
+
terms exactly. If the original mixes Chinese and English, only correct
|
|
32
|
+
the English parts and leave Chinese as-is.
|
|
33
|
+
2. "issues": An array of specific corrections, each with:
|
|
34
|
+
- "type": one of "grammar", "vocabulary", "spelling", "punctuation",
|
|
35
|
+
"style", "word_order"
|
|
36
|
+
- "original": the problematic fragment (quote from original)
|
|
37
|
+
- "corrected": the fixed fragment
|
|
38
|
+
- "rule": a concise explanation in CHINESE (学习者的母语) so the
|
|
39
|
+
developer actually absorbs it. Use grammatical terminology sparingly.
|
|
40
|
+
3. "score": 0-100 fluency score for the original. 100 = native-level.
|
|
41
|
+
Scoring rubric:
|
|
42
|
+
- 90-100: Minor style issues only
|
|
43
|
+
- 70-89: Understandable but has noticeable errors
|
|
44
|
+
- 50-69: Meaning is clear but grammar is systematically broken
|
|
45
|
+
- 30-49: Requires effort to understand
|
|
46
|
+
- 0-29: Largely incomprehensible
|
|
47
|
+
4. "is_clean": boolean, true if no corrections needed (score >= 95)
|
|
48
|
+
|
|
49
|
+
If a prompt is just a code snippet, a file path, or not meaningfully English,
|
|
50
|
+
return {"skip": true} for that entry.
|
|
51
|
+
|
|
52
|
+
Respond with a JSON array matching the input order. No markdown fences.`;
|