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.
Files changed (57) hide show
  1. package/LICENSE +630 -0
  2. package/README.md +287 -0
  3. package/package.json +129 -0
  4. package/plugin/.claude-plugin/CLAUDE.md +4 -0
  5. package/plugin/.claude-plugin/plugin.json +23 -0
  6. package/plugin/.cli-installed +1 -0
  7. package/plugin/CLAUDE.md +6 -0
  8. package/plugin/hooks/CLAUDE.md +6 -0
  9. package/plugin/hooks/bugfixes-2026-01-10.md +92 -0
  10. package/plugin/hooks/hooks.json +79 -0
  11. package/plugin/modes/code--ar.json +24 -0
  12. package/plugin/modes/code--bn.json +24 -0
  13. package/plugin/modes/code--chill.json +8 -0
  14. package/plugin/modes/code--cs.json +24 -0
  15. package/plugin/modes/code--da.json +24 -0
  16. package/plugin/modes/code--de.json +24 -0
  17. package/plugin/modes/code--el.json +24 -0
  18. package/plugin/modes/code--es.json +24 -0
  19. package/plugin/modes/code--fi.json +24 -0
  20. package/plugin/modes/code--fr.json +24 -0
  21. package/plugin/modes/code--he.json +24 -0
  22. package/plugin/modes/code--hi.json +24 -0
  23. package/plugin/modes/code--hu.json +24 -0
  24. package/plugin/modes/code--id.json +24 -0
  25. package/plugin/modes/code--it.json +24 -0
  26. package/plugin/modes/code--ja.json +24 -0
  27. package/plugin/modes/code--ko.json +24 -0
  28. package/plugin/modes/code--nl.json +24 -0
  29. package/plugin/modes/code--no.json +24 -0
  30. package/plugin/modes/code--pl.json +24 -0
  31. package/plugin/modes/code--pt-br.json +24 -0
  32. package/plugin/modes/code--ro.json +24 -0
  33. package/plugin/modes/code--ru.json +24 -0
  34. package/plugin/modes/code--sv.json +24 -0
  35. package/plugin/modes/code--th.json +24 -0
  36. package/plugin/modes/code--tr.json +24 -0
  37. package/plugin/modes/code--uk.json +24 -0
  38. package/plugin/modes/code--ur.json +25 -0
  39. package/plugin/modes/code--vi.json +24 -0
  40. package/plugin/modes/code--zh.json +24 -0
  41. package/plugin/modes/code.json +125 -0
  42. package/plugin/modes/email-investigation.json +120 -0
  43. package/plugin/modes/law-study--chill.json +7 -0
  44. package/plugin/modes/law-study-CLAUDE.md +85 -0
  45. package/plugin/modes/law-study.json +120 -0
  46. package/plugin/package.json +23 -0
  47. package/plugin/scripts/CLAUDE.md +5 -0
  48. package/plugin/scripts/bun-runner.js +176 -0
  49. package/plugin/scripts/mcp-server.cjs +141 -0
  50. package/plugin/scripts/smart-install.js +592 -0
  51. package/plugin/scripts/statusline-counts.js +61 -0
  52. package/plugin/scripts/vl-cli.cjs +104 -0
  53. package/plugin/scripts/worker-cli.js +19 -0
  54. package/plugin/scripts/worker-service.cjs +1919 -0
  55. package/plugin/scripts/worker-wrapper.cjs +2 -0
  56. package/plugin/skills/smart-explore/SKILL.md +145 -0
  57. 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
+ }