vibelearn 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +630 -0
- package/README.md +287 -0
- package/package.json +129 -0
- package/plugin/.claude-plugin/CLAUDE.md +4 -0
- package/plugin/.claude-plugin/plugin.json +23 -0
- package/plugin/.cli-installed +1 -0
- package/plugin/CLAUDE.md +6 -0
- package/plugin/hooks/CLAUDE.md +6 -0
- package/plugin/hooks/bugfixes-2026-01-10.md +92 -0
- package/plugin/hooks/hooks.json +79 -0
- package/plugin/modes/code--ar.json +24 -0
- package/plugin/modes/code--bn.json +24 -0
- package/plugin/modes/code--chill.json +8 -0
- package/plugin/modes/code--cs.json +24 -0
- package/plugin/modes/code--da.json +24 -0
- package/plugin/modes/code--de.json +24 -0
- package/plugin/modes/code--el.json +24 -0
- package/plugin/modes/code--es.json +24 -0
- package/plugin/modes/code--fi.json +24 -0
- package/plugin/modes/code--fr.json +24 -0
- package/plugin/modes/code--he.json +24 -0
- package/plugin/modes/code--hi.json +24 -0
- package/plugin/modes/code--hu.json +24 -0
- package/plugin/modes/code--id.json +24 -0
- package/plugin/modes/code--it.json +24 -0
- package/plugin/modes/code--ja.json +24 -0
- package/plugin/modes/code--ko.json +24 -0
- package/plugin/modes/code--nl.json +24 -0
- package/plugin/modes/code--no.json +24 -0
- package/plugin/modes/code--pl.json +24 -0
- package/plugin/modes/code--pt-br.json +24 -0
- package/plugin/modes/code--ro.json +24 -0
- package/plugin/modes/code--ru.json +24 -0
- package/plugin/modes/code--sv.json +24 -0
- package/plugin/modes/code--th.json +24 -0
- package/plugin/modes/code--tr.json +24 -0
- package/plugin/modes/code--uk.json +24 -0
- package/plugin/modes/code--ur.json +25 -0
- package/plugin/modes/code--vi.json +24 -0
- package/plugin/modes/code--zh.json +24 -0
- package/plugin/modes/code.json +125 -0
- package/plugin/modes/email-investigation.json +120 -0
- package/plugin/modes/law-study--chill.json +7 -0
- package/plugin/modes/law-study-CLAUDE.md +85 -0
- package/plugin/modes/law-study.json +120 -0
- package/plugin/package.json +23 -0
- package/plugin/scripts/CLAUDE.md +5 -0
- package/plugin/scripts/bun-runner.js +176 -0
- package/plugin/scripts/mcp-server.cjs +141 -0
- package/plugin/scripts/smart-install.js +592 -0
- package/plugin/scripts/statusline-counts.js +61 -0
- package/plugin/scripts/vl-cli.cjs +104 -0
- package/plugin/scripts/worker-cli.js +19 -0
- package/plugin/scripts/worker-service.cjs +1919 -0
- package/plugin/scripts/worker-wrapper.cjs +2 -0
- package/plugin/skills/smart-explore/SKILL.md +145 -0
- package/plugin/skills/timeline-report/SKILL.md +91 -0
|
@@ -0,0 +1,592 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Smart Install Script for vibelearn
|
|
4
|
+
*
|
|
5
|
+
* Ensures Bun runtime and uv (Python package manager) are installed
|
|
6
|
+
* (auto-installs if missing) and handles dependency installation when needed.
|
|
7
|
+
*
|
|
8
|
+
* Resolves the install directory from CLAUDE_PLUGIN_ROOT (set by Claude Code
|
|
9
|
+
* for both cache and marketplace installs), falling back to script location
|
|
10
|
+
* and legacy paths.
|
|
11
|
+
*/
|
|
12
|
+
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|
13
|
+
import { execSync, spawnSync } from 'child_process';
|
|
14
|
+
import { join, dirname } from 'path';
|
|
15
|
+
import { homedir } from 'os';
|
|
16
|
+
import { fileURLToPath } from 'url';
|
|
17
|
+
|
|
18
|
+
// Early exit if plugin is disabled in Claude Code settings (#781)
|
|
19
|
+
function isPluginDisabledInClaudeSettings() {
|
|
20
|
+
try {
|
|
21
|
+
const configDir = process.env.CLAUDE_CONFIG_DIR || join(homedir(), '.claude');
|
|
22
|
+
const settingsPath = join(configDir, 'settings.json');
|
|
23
|
+
if (!existsSync(settingsPath)) return false;
|
|
24
|
+
const settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
|
|
25
|
+
return settings?.enabledPlugins?.['anergcorp@vibelearn'] === false;
|
|
26
|
+
} catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (isPluginDisabledInClaudeSettings()) {
|
|
32
|
+
process.exit(0);
|
|
33
|
+
}
|
|
34
|
+
const IS_WINDOWS = process.platform === 'win32';
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Resolve the plugin root directory where dependencies should be installed.
|
|
38
|
+
*
|
|
39
|
+
* Priority:
|
|
40
|
+
* 1. CLAUDE_PLUGIN_ROOT env var (set by Claude Code for hooks — works for
|
|
41
|
+
* both cache-based and marketplace installs)
|
|
42
|
+
* 2. Script location (dirname of this file, up one level from scripts/)
|
|
43
|
+
* 3. XDG path (~/.config/claude/plugins/marketplaces/anergcorp)
|
|
44
|
+
* 4. Legacy path (~/.claude/plugins/marketplaces/anergcorp)
|
|
45
|
+
*/
|
|
46
|
+
function resolveRoot() {
|
|
47
|
+
// CLAUDE_PLUGIN_ROOT is the authoritative location set by Claude Code
|
|
48
|
+
if (process.env.CLAUDE_PLUGIN_ROOT) {
|
|
49
|
+
const root = process.env.CLAUDE_PLUGIN_ROOT;
|
|
50
|
+
if (existsSync(join(root, 'package.json'))) return root;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Derive from script location (this file is in <root>/scripts/)
|
|
54
|
+
try {
|
|
55
|
+
const scriptDir = dirname(fileURLToPath(import.meta.url));
|
|
56
|
+
const candidate = dirname(scriptDir);
|
|
57
|
+
if (existsSync(join(candidate, 'package.json'))) return candidate;
|
|
58
|
+
} catch {
|
|
59
|
+
// import.meta.url not available
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Probe XDG path, then legacy
|
|
63
|
+
const marketplaceRel = join('plugins', 'marketplaces', 'anergcorp');
|
|
64
|
+
const xdg = join(homedir(), '.config', 'claude', marketplaceRel);
|
|
65
|
+
if (existsSync(join(xdg, 'package.json'))) return xdg;
|
|
66
|
+
|
|
67
|
+
return join(homedir(), '.claude', marketplaceRel);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const ROOT = resolveRoot();
|
|
71
|
+
const MARKER = join(ROOT, '.install-version');
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Check if Bun is installed and accessible
|
|
75
|
+
*/
|
|
76
|
+
function isBunInstalled() {
|
|
77
|
+
try {
|
|
78
|
+
const result = spawnSync('bun', ['--version'], {
|
|
79
|
+
encoding: 'utf-8',
|
|
80
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
81
|
+
shell: IS_WINDOWS
|
|
82
|
+
});
|
|
83
|
+
if (result.status === 0) return true;
|
|
84
|
+
} catch {
|
|
85
|
+
// PATH check failed, try common installation paths
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Check common installation paths (handles fresh installs before PATH reload)
|
|
89
|
+
const bunPaths = IS_WINDOWS
|
|
90
|
+
? [join(homedir(), '.bun', 'bin', 'bun.exe')]
|
|
91
|
+
: [join(homedir(), '.bun', 'bin', 'bun'), '/usr/local/bin/bun', '/opt/homebrew/bin/bun'];
|
|
92
|
+
|
|
93
|
+
return bunPaths.some(existsSync);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Get the Bun executable path (from PATH or common install locations)
|
|
98
|
+
*/
|
|
99
|
+
function getBunPath() {
|
|
100
|
+
// Try PATH first
|
|
101
|
+
try {
|
|
102
|
+
const result = spawnSync('bun', ['--version'], {
|
|
103
|
+
encoding: 'utf-8',
|
|
104
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
105
|
+
shell: IS_WINDOWS
|
|
106
|
+
});
|
|
107
|
+
if (result.status === 0) return 'bun';
|
|
108
|
+
} catch {
|
|
109
|
+
// Not in PATH
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Check common installation paths
|
|
113
|
+
const bunPaths = IS_WINDOWS
|
|
114
|
+
? [join(homedir(), '.bun', 'bin', 'bun.exe')]
|
|
115
|
+
: [join(homedir(), '.bun', 'bin', 'bun'), '/usr/local/bin/bun', '/opt/homebrew/bin/bun'];
|
|
116
|
+
|
|
117
|
+
for (const bunPath of bunPaths) {
|
|
118
|
+
if (existsSync(bunPath)) return bunPath;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Minimum required bun version
|
|
126
|
+
* v1.1.14+ required for .changes property and multi-statement SQL support
|
|
127
|
+
*/
|
|
128
|
+
const MIN_BUN_VERSION = '1.1.14';
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Compare semver versions
|
|
132
|
+
*/
|
|
133
|
+
function compareVersions(v1, v2) {
|
|
134
|
+
const parts1 = v1.split('.').map(Number);
|
|
135
|
+
const parts2 = v2.split('.').map(Number);
|
|
136
|
+
for (let i = 0; i < 3; i++) {
|
|
137
|
+
const p1 = parts1[i] || 0;
|
|
138
|
+
const p2 = parts2[i] || 0;
|
|
139
|
+
if (p1 > p2) return 1;
|
|
140
|
+
if (p1 < p2) return -1;
|
|
141
|
+
}
|
|
142
|
+
return 0;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Check if bun version meets minimum requirements
|
|
147
|
+
*/
|
|
148
|
+
function isBunVersionSufficient() {
|
|
149
|
+
const version = getBunVersion();
|
|
150
|
+
if (!version) return false;
|
|
151
|
+
return compareVersions(version, MIN_BUN_VERSION) >= 0;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Get Bun version if installed
|
|
156
|
+
*/
|
|
157
|
+
function getBunVersion() {
|
|
158
|
+
const bunPath = getBunPath();
|
|
159
|
+
if (!bunPath) return null;
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
const result = spawnSync(bunPath, ['--version'], {
|
|
163
|
+
encoding: 'utf-8',
|
|
164
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
165
|
+
shell: IS_WINDOWS
|
|
166
|
+
});
|
|
167
|
+
return result.status === 0 ? result.stdout.trim() : null;
|
|
168
|
+
} catch {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Check if uv is installed and accessible
|
|
175
|
+
*/
|
|
176
|
+
function isUvInstalled() {
|
|
177
|
+
try {
|
|
178
|
+
const result = spawnSync('uv', ['--version'], {
|
|
179
|
+
encoding: 'utf-8',
|
|
180
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
181
|
+
shell: IS_WINDOWS
|
|
182
|
+
});
|
|
183
|
+
if (result.status === 0) return true;
|
|
184
|
+
} catch {
|
|
185
|
+
// PATH check failed, try common installation paths
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Check common installation paths (handles fresh installs before PATH reload)
|
|
189
|
+
const uvPaths = IS_WINDOWS
|
|
190
|
+
? [join(homedir(), '.local', 'bin', 'uv.exe'), join(homedir(), '.cargo', 'bin', 'uv.exe')]
|
|
191
|
+
: [join(homedir(), '.local', 'bin', 'uv'), join(homedir(), '.cargo', 'bin', 'uv'), '/usr/local/bin/uv', '/opt/homebrew/bin/uv'];
|
|
192
|
+
|
|
193
|
+
return uvPaths.some(existsSync);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Get uv version if installed
|
|
198
|
+
*/
|
|
199
|
+
function getUvVersion() {
|
|
200
|
+
try {
|
|
201
|
+
const result = spawnSync('uv', ['--version'], {
|
|
202
|
+
encoding: 'utf-8',
|
|
203
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
204
|
+
shell: IS_WINDOWS
|
|
205
|
+
});
|
|
206
|
+
return result.status === 0 ? result.stdout.trim() : null;
|
|
207
|
+
} catch {
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Install Bun automatically based on platform
|
|
214
|
+
*/
|
|
215
|
+
function installBun() {
|
|
216
|
+
console.error('🔧 Bun not found. Installing Bun runtime...');
|
|
217
|
+
|
|
218
|
+
try {
|
|
219
|
+
if (IS_WINDOWS) {
|
|
220
|
+
// Windows: Use PowerShell installer
|
|
221
|
+
console.error(' Installing via PowerShell...');
|
|
222
|
+
execSync('powershell -c "irm bun.sh/install.ps1 | iex"', {
|
|
223
|
+
stdio: ['pipe', 'pipe', 'inherit'],
|
|
224
|
+
shell: true
|
|
225
|
+
});
|
|
226
|
+
} else {
|
|
227
|
+
// Unix/macOS: Use curl installer
|
|
228
|
+
console.error(' Installing via curl...');
|
|
229
|
+
execSync('curl -fsSL https://bun.sh/install | bash', {
|
|
230
|
+
stdio: ['pipe', 'pipe', 'inherit'],
|
|
231
|
+
shell: true
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Verify installation
|
|
236
|
+
if (isBunInstalled()) {
|
|
237
|
+
const version = getBunVersion();
|
|
238
|
+
console.error(`✅ Bun ${version} installed successfully`);
|
|
239
|
+
return true;
|
|
240
|
+
} else {
|
|
241
|
+
// Bun may be installed but not in PATH yet for this session
|
|
242
|
+
// Try common installation paths
|
|
243
|
+
const bunPaths = IS_WINDOWS
|
|
244
|
+
? [join(homedir(), '.bun', 'bin', 'bun.exe')]
|
|
245
|
+
: [join(homedir(), '.bun', 'bin', 'bun'), '/usr/local/bin/bun', '/opt/homebrew/bin/bun'];
|
|
246
|
+
|
|
247
|
+
for (const bunPath of bunPaths) {
|
|
248
|
+
if (existsSync(bunPath)) {
|
|
249
|
+
console.error(`✅ Bun installed at ${bunPath}`);
|
|
250
|
+
console.error('⚠️ Please restart your terminal or add Bun to PATH:');
|
|
251
|
+
if (IS_WINDOWS) {
|
|
252
|
+
console.error(` $env:Path += ";${join(homedir(), '.bun', 'bin')}"`);
|
|
253
|
+
} else {
|
|
254
|
+
console.error(` export PATH="$HOME/.bun/bin:$PATH"`);
|
|
255
|
+
}
|
|
256
|
+
return true;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
throw new Error('Bun installation completed but binary not found');
|
|
261
|
+
}
|
|
262
|
+
} catch (error) {
|
|
263
|
+
console.error('❌ Failed to install Bun automatically');
|
|
264
|
+
console.error(' Please install manually:');
|
|
265
|
+
if (IS_WINDOWS) {
|
|
266
|
+
console.error(' - winget install Oven-sh.Bun');
|
|
267
|
+
console.error(' - Or: powershell -c "irm bun.sh/install.ps1 | iex"');
|
|
268
|
+
} else {
|
|
269
|
+
console.error(' - curl -fsSL https://bun.sh/install | bash');
|
|
270
|
+
console.error(' - Or: brew install oven-sh/bun/bun');
|
|
271
|
+
}
|
|
272
|
+
console.error(' Then restart your terminal and try again.');
|
|
273
|
+
throw error;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Install uv automatically based on platform
|
|
279
|
+
*/
|
|
280
|
+
function installUv() {
|
|
281
|
+
console.error('🐍 Installing uv for Python/Chroma support...');
|
|
282
|
+
|
|
283
|
+
try {
|
|
284
|
+
if (IS_WINDOWS) {
|
|
285
|
+
// Windows: Use PowerShell installer
|
|
286
|
+
console.error(' Installing via PowerShell...');
|
|
287
|
+
execSync('powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"', {
|
|
288
|
+
stdio: ['pipe', 'pipe', 'inherit'],
|
|
289
|
+
shell: true
|
|
290
|
+
});
|
|
291
|
+
} else {
|
|
292
|
+
// Unix/macOS: Use curl installer
|
|
293
|
+
console.error(' Installing via curl...');
|
|
294
|
+
execSync('curl -LsSf https://astral.sh/uv/install.sh | sh', {
|
|
295
|
+
stdio: ['pipe', 'pipe', 'inherit'],
|
|
296
|
+
shell: true
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Verify installation
|
|
301
|
+
if (isUvInstalled()) {
|
|
302
|
+
const version = getUvVersion();
|
|
303
|
+
console.error(`✅ uv ${version} installed successfully`);
|
|
304
|
+
return true;
|
|
305
|
+
} else {
|
|
306
|
+
// uv may be installed but not in PATH yet for this session
|
|
307
|
+
// Try common installation paths
|
|
308
|
+
const uvPaths = IS_WINDOWS
|
|
309
|
+
? [join(homedir(), '.local', 'bin', 'uv.exe'), join(homedir(), '.cargo', 'bin', 'uv.exe')]
|
|
310
|
+
: [join(homedir(), '.local', 'bin', 'uv'), join(homedir(), '.cargo', 'bin', 'uv'), '/usr/local/bin/uv', '/opt/homebrew/bin/uv'];
|
|
311
|
+
|
|
312
|
+
for (const uvPath of uvPaths) {
|
|
313
|
+
if (existsSync(uvPath)) {
|
|
314
|
+
console.error(`✅ uv installed at ${uvPath}`);
|
|
315
|
+
console.error('⚠️ Please restart your terminal or add uv to PATH:');
|
|
316
|
+
if (IS_WINDOWS) {
|
|
317
|
+
console.error(` $env:Path += ";${join(homedir(), '.local', 'bin')}"`);
|
|
318
|
+
} else {
|
|
319
|
+
console.error(` export PATH="$HOME/.local/bin:$PATH"`);
|
|
320
|
+
}
|
|
321
|
+
return true;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
throw new Error('uv installation completed but binary not found');
|
|
326
|
+
}
|
|
327
|
+
} catch (error) {
|
|
328
|
+
console.error('❌ Failed to install uv automatically');
|
|
329
|
+
console.error(' Please install manually:');
|
|
330
|
+
if (IS_WINDOWS) {
|
|
331
|
+
console.error(' - winget install astral-sh.uv');
|
|
332
|
+
console.error(' - Or: powershell -c "irm https://astral.sh/uv/install.ps1 | iex"');
|
|
333
|
+
} else {
|
|
334
|
+
console.error(' - curl -LsSf https://astral.sh/uv/install.sh | sh');
|
|
335
|
+
console.error(' - Or: brew install uv (macOS)');
|
|
336
|
+
}
|
|
337
|
+
console.error(' Then restart your terminal and try again.');
|
|
338
|
+
throw error;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Add shell alias for vibelearn command
|
|
344
|
+
*/
|
|
345
|
+
function installCLI() {
|
|
346
|
+
const WORKER_CLI = join(ROOT, 'scripts', 'worker-service.cjs');
|
|
347
|
+
const bunPath = getBunPath() || 'bun';
|
|
348
|
+
const aliasLine = `alias vibelearn='${bunPath} "${WORKER_CLI}"'`;
|
|
349
|
+
const markerPath = join(ROOT, '.cli-installed');
|
|
350
|
+
|
|
351
|
+
// Skip if already installed
|
|
352
|
+
if (existsSync(markerPath)) return;
|
|
353
|
+
|
|
354
|
+
try {
|
|
355
|
+
if (IS_WINDOWS) {
|
|
356
|
+
// Windows: Add to PATH via PowerShell profile
|
|
357
|
+
const profilePath = join(process.env.USERPROFILE || homedir(), 'Documents', 'PowerShell', 'Microsoft.PowerShell_profile.ps1');
|
|
358
|
+
const profileDir = join(process.env.USERPROFILE || homedir(), 'Documents', 'PowerShell');
|
|
359
|
+
const functionDef = `function vibelearn { & "${bunPath}" "${WORKER_CLI}" $args }\n`;
|
|
360
|
+
|
|
361
|
+
if (!existsSync(profileDir)) {
|
|
362
|
+
execSync(`mkdir "${profileDir}"`, { stdio: 'ignore', shell: true });
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const existingContent = existsSync(profilePath) ? readFileSync(profilePath, 'utf-8') : '';
|
|
366
|
+
if (!existingContent.includes('function vibelearn')) {
|
|
367
|
+
writeFileSync(profilePath, existingContent + '\n' + functionDef);
|
|
368
|
+
console.error(`✅ PowerShell function added to profile`);
|
|
369
|
+
console.error(' Restart your terminal to use: vibelearn <command>');
|
|
370
|
+
}
|
|
371
|
+
} else {
|
|
372
|
+
// Unix: Add alias to shell configs
|
|
373
|
+
const shellConfigs = [
|
|
374
|
+
join(homedir(), '.bashrc'),
|
|
375
|
+
join(homedir(), '.zshrc')
|
|
376
|
+
];
|
|
377
|
+
|
|
378
|
+
for (const config of shellConfigs) {
|
|
379
|
+
if (existsSync(config)) {
|
|
380
|
+
const content = readFileSync(config, 'utf-8');
|
|
381
|
+
if (!content.includes('alias vibelearn=')) {
|
|
382
|
+
writeFileSync(config, content + '\n' + aliasLine + '\n');
|
|
383
|
+
console.error(`✅ Alias added to ${config}`);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
console.error(' Restart your terminal to use: vibelearn <command>');
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
writeFileSync(markerPath, new Date().toISOString());
|
|
391
|
+
} catch (error) {
|
|
392
|
+
console.error(`⚠️ Could not add shell alias: ${error.message}`);
|
|
393
|
+
console.error(` Use directly: ${bunPath} "${WORKER_CLI}" <command>`);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Check if dependencies need to be installed
|
|
399
|
+
*/
|
|
400
|
+
function needsInstall() {
|
|
401
|
+
if (!existsSync(join(ROOT, 'node_modules'))) return true;
|
|
402
|
+
try {
|
|
403
|
+
const pkg = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf-8'));
|
|
404
|
+
const marker = JSON.parse(readFileSync(MARKER, 'utf-8'));
|
|
405
|
+
return pkg.version !== marker.version || getBunVersion() !== marker.bun;
|
|
406
|
+
} catch {
|
|
407
|
+
return true;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Install dependencies using Bun with npm fallback
|
|
413
|
+
*
|
|
414
|
+
* Bun has issues with npm alias packages (e.g., string-width-cjs, strip-ansi-cjs)
|
|
415
|
+
* that are defined in package-lock.json. When bun fails with 404 errors for these
|
|
416
|
+
* packages, we fall back to npm which handles aliases correctly.
|
|
417
|
+
*/
|
|
418
|
+
function installDeps() {
|
|
419
|
+
const bunPath = getBunPath();
|
|
420
|
+
if (!bunPath) {
|
|
421
|
+
throw new Error('Bun executable not found');
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
console.error('📦 Installing dependencies with Bun...');
|
|
425
|
+
|
|
426
|
+
// Quote path for Windows paths with spaces
|
|
427
|
+
const bunCmd = IS_WINDOWS && bunPath.includes(' ') ? `"${bunPath}"` : bunPath;
|
|
428
|
+
|
|
429
|
+
// Use pipe for stdout to prevent non-JSON output leaking to Claude Code hooks.
|
|
430
|
+
// stderr is inherited so progress/errors are still visible to the user.
|
|
431
|
+
const installStdio = ['pipe', 'pipe', 'inherit'];
|
|
432
|
+
|
|
433
|
+
let bunSucceeded = false;
|
|
434
|
+
try {
|
|
435
|
+
execSync(`${bunCmd} install`, { cwd: ROOT, stdio: installStdio, shell: IS_WINDOWS });
|
|
436
|
+
bunSucceeded = true;
|
|
437
|
+
} catch {
|
|
438
|
+
// First attempt failed, try with force flag
|
|
439
|
+
try {
|
|
440
|
+
execSync(`${bunCmd} install --force`, { cwd: ROOT, stdio: installStdio, shell: IS_WINDOWS });
|
|
441
|
+
bunSucceeded = true;
|
|
442
|
+
} catch {
|
|
443
|
+
// Bun failed completely, will try npm fallback
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Fallback to npm if bun failed (handles npm alias packages correctly)
|
|
448
|
+
if (!bunSucceeded) {
|
|
449
|
+
console.error('⚠️ Bun install failed, falling back to npm...');
|
|
450
|
+
console.error(' (This can happen with npm alias packages like *-cjs)');
|
|
451
|
+
try {
|
|
452
|
+
execSync('npm install', { cwd: ROOT, stdio: installStdio, shell: IS_WINDOWS });
|
|
453
|
+
} catch (npmError) {
|
|
454
|
+
throw new Error('Both bun and npm install failed: ' + npmError.message);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Write version marker
|
|
459
|
+
const pkg = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf-8'));
|
|
460
|
+
writeFileSync(MARKER, JSON.stringify({
|
|
461
|
+
version: pkg.version,
|
|
462
|
+
bun: getBunVersion(),
|
|
463
|
+
uv: getUvVersion(),
|
|
464
|
+
installedAt: new Date().toISOString()
|
|
465
|
+
}));
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Verify that critical runtime modules are resolvable from the install directory.
|
|
470
|
+
* Returns true if all critical modules exist, false otherwise.
|
|
471
|
+
*/
|
|
472
|
+
function verifyCriticalModules() {
|
|
473
|
+
const pkg = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf-8'));
|
|
474
|
+
const dependencies = Object.keys(pkg.dependencies || {});
|
|
475
|
+
|
|
476
|
+
const missing = [];
|
|
477
|
+
for (const dep of dependencies) {
|
|
478
|
+
// Check that the module directory exists in node_modules
|
|
479
|
+
const modulePath = join(ROOT, 'node_modules', ...dep.split('/'));
|
|
480
|
+
if (!existsSync(modulePath)) {
|
|
481
|
+
missing.push(dep);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
if (missing.length > 0) {
|
|
486
|
+
console.error(`❌ Post-install check failed: missing modules: ${missing.join(', ')}`);
|
|
487
|
+
return false;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
return true;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Main execution
|
|
494
|
+
try {
|
|
495
|
+
// Step 1: Ensure Bun is installed and meets minimum version (REQUIRED)
|
|
496
|
+
if (!isBunInstalled()) {
|
|
497
|
+
installBun();
|
|
498
|
+
|
|
499
|
+
// Re-check after installation
|
|
500
|
+
if (!isBunInstalled()) {
|
|
501
|
+
console.error('❌ Bun is required but not available in PATH');
|
|
502
|
+
console.error(' Please restart your terminal after installation');
|
|
503
|
+
process.exit(1);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// Step 1.5: Ensure Bun version is sufficient
|
|
508
|
+
if (!isBunVersionSufficient()) {
|
|
509
|
+
const currentVersion = getBunVersion();
|
|
510
|
+
console.error(`⚠️ Bun ${currentVersion} is outdated. Minimum required: ${MIN_BUN_VERSION}`);
|
|
511
|
+
console.error(' Upgrading bun...');
|
|
512
|
+
try {
|
|
513
|
+
execSync('bun upgrade', { stdio: ['pipe', 'pipe', 'inherit'], shell: IS_WINDOWS });
|
|
514
|
+
if (!isBunVersionSufficient()) {
|
|
515
|
+
console.error(`❌ Bun upgrade failed. Please manually upgrade: bun upgrade`);
|
|
516
|
+
process.exit(1);
|
|
517
|
+
}
|
|
518
|
+
console.error(`✅ Bun upgraded to ${getBunVersion()}`);
|
|
519
|
+
} catch (error) {
|
|
520
|
+
console.error(`❌ Failed to upgrade bun: ${error.message}`);
|
|
521
|
+
console.error(' Please manually upgrade: bun upgrade');
|
|
522
|
+
process.exit(1);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Step 2: Ensure uv is installed (REQUIRED for vector search)
|
|
527
|
+
if (!isUvInstalled()) {
|
|
528
|
+
installUv();
|
|
529
|
+
|
|
530
|
+
// Re-check after installation
|
|
531
|
+
if (!isUvInstalled()) {
|
|
532
|
+
console.error('❌ uv is required but not available in PATH');
|
|
533
|
+
console.error(' Please restart your terminal after installation');
|
|
534
|
+
process.exit(1);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// Step 3: Install dependencies if needed
|
|
539
|
+
if (needsInstall()) {
|
|
540
|
+
const pkg = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf-8'));
|
|
541
|
+
const newVersion = pkg.version;
|
|
542
|
+
|
|
543
|
+
installDeps();
|
|
544
|
+
|
|
545
|
+
// Verify critical modules are resolvable
|
|
546
|
+
if (!verifyCriticalModules()) {
|
|
547
|
+
console.error('⚠️ Retrying install with npm...');
|
|
548
|
+
try {
|
|
549
|
+
execSync('npm install --production', { cwd: ROOT, stdio: ['pipe', 'pipe', 'inherit'], shell: IS_WINDOWS });
|
|
550
|
+
} catch {
|
|
551
|
+
// npm also failed
|
|
552
|
+
}
|
|
553
|
+
if (!verifyCriticalModules()) {
|
|
554
|
+
console.error('❌ Dependencies could not be installed. Plugin may not work correctly.');
|
|
555
|
+
process.exit(1);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
console.error('✅ Dependencies installed');
|
|
560
|
+
|
|
561
|
+
// Auto-restart worker to pick up new code
|
|
562
|
+
const port = process.env.VIBELEARN_WORKER_PORT || 37778;
|
|
563
|
+
console.error(`[vibelearn] Plugin updated to v${newVersion} - restarting worker...`);
|
|
564
|
+
try {
|
|
565
|
+
// Graceful shutdown via HTTP (curl is cross-platform enough)
|
|
566
|
+
execSync(`curl -s -X POST http://127.0.0.1:${port}/api/admin/shutdown`, {
|
|
567
|
+
stdio: 'ignore',
|
|
568
|
+
shell: IS_WINDOWS,
|
|
569
|
+
timeout: 5000
|
|
570
|
+
});
|
|
571
|
+
// Brief wait for port to free
|
|
572
|
+
execSync(IS_WINDOWS ? 'timeout /t 1 /nobreak >nul' : 'sleep 0.5', {
|
|
573
|
+
stdio: 'ignore',
|
|
574
|
+
shell: true
|
|
575
|
+
});
|
|
576
|
+
} catch {
|
|
577
|
+
// Worker wasn't running or already stopped - that's fine
|
|
578
|
+
}
|
|
579
|
+
// Worker will be started fresh by next hook in chain (worker-service.cjs start)
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// Step 4: Install CLI to PATH
|
|
583
|
+
installCLI();
|
|
584
|
+
|
|
585
|
+
// Output valid JSON for Claude Code hook contract
|
|
586
|
+
console.log(JSON.stringify({ continue: true, suppressOutput: true }));
|
|
587
|
+
} catch (e) {
|
|
588
|
+
console.error('❌ Installation failed:', e.message);
|
|
589
|
+
// Still output valid JSON so Claude Code doesn't show a confusing error
|
|
590
|
+
console.log(JSON.stringify({ continue: true, suppressOutput: true }));
|
|
591
|
+
process.exit(1);
|
|
592
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Statusline Counts — lightweight project-scoped observation counter
|
|
4
|
+
*
|
|
5
|
+
* Returns JSON with observation and prompt counts for the given project,
|
|
6
|
+
* suitable for integration into Claude Code's statusLineCommand.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* bun statusline-counts.js <cwd>
|
|
10
|
+
* bun statusline-counts.js /home/user/my-project
|
|
11
|
+
*
|
|
12
|
+
* Output (JSON, stdout):
|
|
13
|
+
* {"observations": 42, "prompts": 15, "project": "my-project"}
|
|
14
|
+
*
|
|
15
|
+
* The project name is derived from basename(cwd). Observations are counted
|
|
16
|
+
* with a WHERE project = ? filter so only the current project's data is
|
|
17
|
+
* returned — preventing inflated counts from cross-project observations.
|
|
18
|
+
*
|
|
19
|
+
* Performance: ~10ms typical (direct SQLite read, no HTTP, no worker dependency)
|
|
20
|
+
*/
|
|
21
|
+
import { Database } from "bun:sqlite";
|
|
22
|
+
import { existsSync, readFileSync } from "fs";
|
|
23
|
+
import { homedir } from "os";
|
|
24
|
+
import { join, basename } from "path";
|
|
25
|
+
|
|
26
|
+
const cwd = process.argv[2] || process.env.CLAUDE_CWD || process.cwd();
|
|
27
|
+
const project = basename(cwd);
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
// Resolve data directory: env var → settings.json → default
|
|
31
|
+
let dataDir = process.env.VIBELEARN_DATA_DIR || join(homedir(), ".vibelearn");
|
|
32
|
+
if (!process.env.VIBELEARN_DATA_DIR) {
|
|
33
|
+
const settingsPath = join(dataDir, "settings.json");
|
|
34
|
+
if (existsSync(settingsPath)) {
|
|
35
|
+
try {
|
|
36
|
+
const settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
37
|
+
if (settings.VIBELEARN_DATA_DIR) dataDir = settings.VIBELEARN_DATA_DIR;
|
|
38
|
+
} catch { /* use default */ }
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const dbPath = join(dataDir, "vibelearn.db");
|
|
43
|
+
if (!existsSync(dbPath)) {
|
|
44
|
+
console.log(JSON.stringify({ observations: 0, prompts: 0, project }));
|
|
45
|
+
process.exit(0);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const db = new Database(dbPath, { readonly: true });
|
|
49
|
+
|
|
50
|
+
const obs = db.query("SELECT COUNT(*) as c FROM observations WHERE project = ?").get(project);
|
|
51
|
+
// user_prompts links to projects through sdk_sessions.content_session_id
|
|
52
|
+
const prompts = db.query(
|
|
53
|
+
`SELECT COUNT(*) as c FROM user_prompts up
|
|
54
|
+
JOIN sdk_sessions s ON s.content_session_id = up.content_session_id
|
|
55
|
+
WHERE s.project = ?`
|
|
56
|
+
).get(project);
|
|
57
|
+
console.log(JSON.stringify({ observations: obs.c, prompts: prompts.c, project }));
|
|
58
|
+
db.close();
|
|
59
|
+
} catch (e) {
|
|
60
|
+
console.log(JSON.stringify({ observations: 0, prompts: 0, project, error: e.message }));
|
|
61
|
+
}
|