bluera-knowledge 0.13.2 → 0.14.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/dist/index.js CHANGED
@@ -7,10 +7,10 @@ import {
7
7
  isWebStoreDefinition,
8
8
  runMCPServer,
9
9
  spawnBackgroundWorker
10
- } from "./chunk-H5AKKHY7.js";
10
+ } from "./chunk-UAWKTJWN.js";
11
11
  import {
12
12
  IntelligentCrawler
13
- } from "./chunk-GCUKVV33.js";
13
+ } from "./chunk-AIS5S77C.js";
14
14
  import {
15
15
  ASTParser,
16
16
  AdapterRegistry,
@@ -24,7 +24,7 @@ import {
24
24
  err,
25
25
  extractRepoName,
26
26
  ok
27
- } from "./chunk-6ZVW2P2F.js";
27
+ } from "./chunk-Y24ZJRZP.js";
28
28
  import "./chunk-HRQD3MPH.js";
29
29
 
30
30
  // src/index.ts
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  createMCPServer,
3
3
  runMCPServer
4
- } from "../chunk-H5AKKHY7.js";
5
- import "../chunk-6ZVW2P2F.js";
4
+ } from "../chunk-UAWKTJWN.js";
5
+ import "../chunk-Y24ZJRZP.js";
6
6
  import "../chunk-HRQD3MPH.js";
7
7
  export {
8
8
  createMCPServer,
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  IntelligentCrawler
4
- } from "../chunk-GCUKVV33.js";
4
+ } from "../chunk-AIS5S77C.js";
5
5
  import {
6
6
  JobService,
7
7
  createDocumentId,
@@ -10,7 +10,7 @@ import {
10
10
  createStoreId,
11
11
  destroyServices,
12
12
  shutdownLogger
13
- } from "../chunk-6ZVW2P2F.js";
13
+ } from "../chunk-Y24ZJRZP.js";
14
14
  import "../chunk-HRQD3MPH.js";
15
15
 
16
16
  // src/workers/background-worker-cli.ts
@@ -3,12 +3,12 @@
3
3
  # Automatically checks and installs dependencies for the plugin
4
4
  #
5
5
  # Environment variables:
6
- # BK_SKIP_AUTO_INSTALL=1 - Skip automatic pip installation of crawl4ai
6
+ # BK_SKIP_AUTO_INSTALL=1 - Skip automatic installation of crawl4ai
7
7
  # Set this if you prefer to manage Python packages manually
8
8
  #
9
9
  # What this script auto-installs (if missing):
10
10
  # - Node.js dependencies (via bun or npm, from package.json)
11
- # - crawl4ai Python package (via pip, for web crawling)
11
+ # - Python virtual environment with crawl4ai (isolated from system Python)
12
12
  # - Playwright Chromium browser (via playwright CLI, for headless crawling)
13
13
 
14
14
  set -e
@@ -16,6 +16,11 @@ set -e
16
16
  # Get the plugin root directory
17
17
  PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT:-$(dirname "$(dirname "$0")")}"
18
18
 
19
+ # Venv location within plugin (isolated from system Python)
20
+ VENV_DIR="$PLUGIN_ROOT/.venv"
21
+ VENV_PYTHON="$VENV_DIR/bin/python3"
22
+ VENV_PIP="$VENV_DIR/bin/pip"
23
+
19
24
  # Colors for output
20
25
  RED='\033[0;31m'
21
26
  GREEN='\033[0;32m'
@@ -26,21 +31,24 @@ NC='\033[0m' # No Color
26
31
  # Helper Functions
27
32
  # =====================
28
33
 
29
- # Install Playwright browser (called after crawl4ai is confirmed installed)
34
+ # Install Playwright browser using specified python
35
+ # Args: $1 = python path to use
30
36
  install_playwright_browser() {
37
+ local PYTHON_CMD="${1:-python3}"
38
+
31
39
  # Check if Playwright Chromium is already installed by testing if browser can be launched
32
- if python3 -c "from playwright.sync_api import sync_playwright; p = sync_playwright().start(); b = p.chromium.launch(); b.close(); p.stop()" 2>/dev/null; then
40
+ if "$PYTHON_CMD" -c "from playwright.sync_api import sync_playwright; p = sync_playwright().start(); b = p.chromium.launch(); b.close(); p.stop()" 2>/dev/null; then
33
41
  echo -e "${GREEN}[bluera-knowledge] Playwright Chromium ready ✓${NC}"
34
42
  return 0
35
43
  fi
36
44
 
37
45
  echo -e "${YELLOW}[bluera-knowledge] Installing Playwright browser (one-time setup)...${NC}"
38
- if python3 -m playwright install chromium 2>/dev/null; then
46
+ if "$PYTHON_CMD" -m playwright install chromium 2>/dev/null; then
39
47
  echo -e "${GREEN}[bluera-knowledge] Playwright Chromium installed ✓${NC}"
40
48
  return 0
41
49
  else
42
50
  echo -e "${YELLOW}[bluera-knowledge] Playwright browser install failed.${NC}"
43
- echo -e "${YELLOW}Manual fix: python3 -m playwright install chromium${NC}"
51
+ echo -e "${YELLOW}Manual fix: $PYTHON_CMD -m playwright install chromium${NC}"
44
52
  return 1
45
53
  fi
46
54
  }
@@ -68,7 +76,7 @@ if [ ! -d "$PLUGIN_ROOT/node_modules" ]; then
68
76
  fi
69
77
 
70
78
  # =====================
71
- # Python Dependencies
79
+ # Python Dependencies (using venv)
72
80
  # =====================
73
81
 
74
82
  # Check if Python3 is installed
@@ -81,75 +89,56 @@ fi
81
89
 
82
90
  # Check Python version (require 3.8+)
83
91
  python_version=$(python3 -c 'import sys; print(".".join(map(str, sys.version_info[:2])))')
84
- required_version="3.8"
85
92
 
86
93
  if ! python3 -c "import sys; exit(0 if sys.version_info >= (3, 8) else 1)"; then
87
94
  echo -e "${YELLOW}[bluera-knowledge] Python ${python_version} detected. Python 3.8+ recommended for crawl4ai${NC}"
88
95
  fi
89
96
 
90
- # Check if crawl4ai is installed
91
- if python3 -c "import crawl4ai" 2>/dev/null; then
92
- # Already installed - get version
93
- crawl4ai_version=$(python3 -c "import crawl4ai; print(crawl4ai.__version__)" 2>/dev/null || echo "unknown")
94
- echo -e "${GREEN}[bluera-knowledge] crawl4ai ${crawl4ai_version} is installed ✓${NC}"
97
+ # Check if venv exists and has crawl4ai installed
98
+ if [ -f "$VENV_PYTHON" ] && "$VENV_PYTHON" -c "import crawl4ai" 2>/dev/null; then
99
+ crawl4ai_version=$("$VENV_PYTHON" -c "import crawl4ai; print(crawl4ai.__version__)" 2>/dev/null || echo "unknown")
100
+ echo -e "${GREEN}[bluera-knowledge] crawl4ai ${crawl4ai_version} ready (venv) ✓${NC}"
95
101
  # Ensure Playwright browser is installed for headless crawling
96
- install_playwright_browser
102
+ install_playwright_browser "$VENV_PYTHON"
97
103
  exit 0
98
104
  fi
99
105
 
100
- # crawl4ai not installed - attempt installation
101
- echo -e "${YELLOW}[bluera-knowledge] crawl4ai not found. Web crawling features will be unavailable.${NC}"
102
- echo ""
103
- echo -e "${YELLOW}To enable web crawling, install crawl4ai:${NC}"
104
- echo -e " ${GREEN}pip install crawl4ai${NC}"
105
- echo ""
106
-
107
106
  # Check if auto-install is disabled
108
107
  if [ "${BK_SKIP_AUTO_INSTALL:-}" = "1" ]; then
109
108
  echo -e "${YELLOW}[bluera-knowledge] Auto-install disabled (BK_SKIP_AUTO_INSTALL=1)${NC}"
110
- echo -e "${YELLOW}Install manually: pip install crawl4ai && python3 -m playwright install chromium${NC}"
109
+ echo -e "${YELLOW}To enable web crawling, create venv manually:${NC}"
110
+ echo -e " ${GREEN}python3 -m venv $VENV_DIR${NC}"
111
+ echo -e " ${GREEN}$VENV_PIP install crawl4ai${NC}"
112
+ echo -e " ${GREEN}$VENV_PYTHON -m playwright install chromium${NC}"
111
113
  exit 0
112
114
  fi
113
115
 
114
- # Check if we should auto-install
115
- if command -v pip3 &> /dev/null || command -v pip &> /dev/null; then
116
- echo -e "${YELLOW}[bluera-knowledge] Auto-installing crawl4ai via pip...${NC}"
117
- echo -e "${YELLOW}(Set BK_SKIP_AUTO_INSTALL=1 to disable auto-install)${NC}"
118
-
119
- # Try to install using pip3 or pip
120
- if command -v pip3 &> /dev/null; then
121
- PIP_CMD="pip3"
116
+ # Create venv if it doesn't exist
117
+ if [ ! -d "$VENV_DIR" ]; then
118
+ echo -e "${YELLOW}[bluera-knowledge] Creating Python virtual environment...${NC}"
119
+ if python3 -m venv "$VENV_DIR" 2>/dev/null; then
120
+ echo -e "${GREEN}[bluera-knowledge] Virtual environment created ✓${NC}"
122
121
  else
123
- PIP_CMD="pip"
122
+ echo -e "${RED}[bluera-knowledge] Failed to create virtual environment${NC}"
123
+ echo -e "${YELLOW}Manual fix: python3 -m venv $VENV_DIR${NC}"
124
+ exit 0
124
125
  fi
126
+ fi
125
127
 
126
- # Try with --break-system-packages for PEP 668 environments (Python 3.11+)
127
- # This is needed on modern Python versions (macOS Homebrew, some Linux distros)
128
- if $PIP_CMD install --quiet --break-system-packages crawl4ai 2>/dev/null; then
129
- echo -e "${GREEN}[bluera-knowledge] Successfully installed crawl4ai ✓${NC}"
130
- crawl4ai_version=$(python3 -c "import crawl4ai; print(crawl4ai.__version__)" 2>/dev/null || echo "installed")
131
- echo -e "${GREEN}[bluera-knowledge] crawl4ai ${crawl4ai_version} ready${NC}"
132
- # Install Playwright browser for headless crawling
133
- install_playwright_browser
134
- else
135
- # Fallback: try without --break-system-packages for older Python
136
- if $PIP_CMD install --quiet --user crawl4ai 2>/dev/null; then
137
- echo -e "${GREEN}[bluera-knowledge] Successfully installed crawl4ai ✓${NC}"
138
- crawl4ai_version=$(python3 -c "import crawl4ai; print(crawl4ai.__version__)" 2>/dev/null || echo "installed")
139
- echo -e "${GREEN}[bluera-knowledge] crawl4ai ${crawl4ai_version} ready${NC}"
140
- # Install Playwright browser for headless crawling
141
- install_playwright_browser
142
- else
143
- echo -e "${RED}[bluera-knowledge] Auto-installation failed${NC}"
144
- echo ""
145
- echo -e "${YELLOW}For Python 3.11+ (externally-managed), install manually:${NC}"
146
- echo -e " ${GREEN}pip install --break-system-packages crawl4ai${NC}"
147
- echo -e "${YELLOW}Or use a virtual environment:${NC}"
148
- echo -e " ${GREEN}python3 -m venv venv && source venv/bin/activate && pip install crawl4ai${NC}"
149
- fi
150
- fi
128
+ # Install crawl4ai into venv
129
+ echo -e "${YELLOW}[bluera-knowledge] Installing crawl4ai into virtual environment...${NC}"
130
+ echo -e "${YELLOW}(Set BK_SKIP_AUTO_INSTALL=1 to disable auto-install)${NC}"
131
+
132
+ if "$VENV_PIP" install --quiet crawl4ai 2>/dev/null; then
133
+ crawl4ai_version=$("$VENV_PYTHON" -c "import crawl4ai; print(crawl4ai.__version__)" 2>/dev/null || echo "installed")
134
+ echo -e "${GREEN}[bluera-knowledge] crawl4ai ${crawl4ai_version} installed (venv) ✓${NC}"
135
+ # Install Playwright browser for headless crawling
136
+ install_playwright_browser "$VENV_PYTHON"
151
137
  else
152
- echo -e "${YELLOW}pip not found. Please install crawl4ai manually.${NC}"
138
+ echo -e "${RED}[bluera-knowledge] Failed to install crawl4ai${NC}"
139
+ echo -e "${YELLOW}Manual fix:${NC}"
140
+ echo -e " ${GREEN}$VENV_PIP install crawl4ai${NC}"
141
+ echo -e " ${GREEN}$VENV_PYTHON -m playwright install chromium${NC}"
153
142
  fi
154
143
 
155
144
  # Always exit 0 to not block the session
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bluera-knowledge",
3
- "version": "0.13.2",
3
+ "version": "0.14.0",
4
4
  "description": "CLI tool for managing knowledge stores with semantic search",
5
5
  "type": "module",
6
6
  "bin": {
@@ -78,7 +78,7 @@ describe('PythonBridge', () => {
78
78
 
79
79
  expect(spawn).toHaveBeenCalledWith(
80
80
  'python3',
81
- ['python/crawl_worker.py'],
81
+ [expect.stringMatching(/.*\/python\/crawl_worker\.py$/)],
82
82
  expect.objectContaining({
83
83
  stdio: ['pipe', 'pipe', 'pipe'],
84
84
  })
@@ -1,6 +1,9 @@
1
1
  import { spawn, type ChildProcess } from 'node:child_process';
2
2
  import { randomUUID } from 'node:crypto';
3
+ import { existsSync } from 'node:fs';
4
+ import path from 'node:path';
3
5
  import { createInterface, type Interface as ReadlineInterface } from 'node:readline';
6
+ import { fileURLToPath } from 'node:url';
4
7
  import { ZodError } from 'zod';
5
8
  import {
6
9
  type CrawlResult,
@@ -37,9 +40,39 @@ export class PythonBridge {
37
40
  start(): Promise<void> {
38
41
  if (this.process) return Promise.resolve();
39
42
 
40
- logger.debug('Starting Python bridge process');
43
+ // Compute absolute path to Python worker using import.meta.url
44
+ // This works both in development (src/) and production (dist/)
45
+ const currentFilePath = fileURLToPath(import.meta.url);
46
+ const isProduction = currentFilePath.includes('/dist/');
47
+
48
+ let pythonWorkerPath: string;
49
+ let pythonPath: string;
50
+
51
+ if (isProduction) {
52
+ // Production: Find dist dir and go to sibling python/ directory
53
+ const distIndex = currentFilePath.indexOf('/dist/');
54
+ const pluginRoot = currentFilePath.substring(0, distIndex);
55
+ pythonWorkerPath = path.join(pluginRoot, 'python', 'crawl_worker.py');
56
+
57
+ // Use venv python if available (installed by check-dependencies.sh hook)
58
+ const venvPython = path.join(pluginRoot, '.venv', 'bin', 'python3');
59
+ pythonPath = existsSync(venvPython) ? venvPython : 'python3';
60
+ } else {
61
+ // Development: Go up from src/crawl to find python/
62
+ const srcDir = path.dirname(path.dirname(currentFilePath));
63
+ const projectRoot = path.dirname(srcDir);
64
+ pythonWorkerPath = path.join(projectRoot, 'python', 'crawl_worker.py');
65
+
66
+ // Development: Use system python (user manages their own environment)
67
+ pythonPath = 'python3';
68
+ }
69
+
70
+ logger.debug(
71
+ { pythonWorkerPath, pythonPath, currentFilePath, isProduction },
72
+ 'Starting Python bridge process'
73
+ );
41
74
 
42
- this.process = spawn('python3', ['python/crawl_worker.py'], {
75
+ this.process = spawn(pythonPath, [pythonWorkerPath], {
43
76
  stdio: ['pipe', 'pipe', 'pipe'],
44
77
  });
45
78
 
@@ -10,12 +10,14 @@ import { metaCommands } from './meta.commands.js';
10
10
  import { commandRegistry } from './registry.js';
11
11
  import { storeCommands } from './store.commands.js';
12
12
  import { syncCommands } from './sync.commands.js';
13
+ import { uninstallCommands } from './uninstall.commands.js';
13
14
 
14
15
  // Register all commands
15
16
  commandRegistry.registerAll(storeCommands);
16
17
  commandRegistry.registerAll(jobCommands);
17
18
  commandRegistry.registerAll(metaCommands);
18
19
  commandRegistry.registerAll(syncCommands);
20
+ commandRegistry.registerAll(uninstallCommands);
19
21
 
20
22
  // Re-export for convenience
21
23
  export { commandRegistry, executeCommand, generateHelp } from './registry.js';
@@ -0,0 +1,37 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { uninstallCommands } from './uninstall.commands.js';
3
+
4
+ describe('uninstall.commands', () => {
5
+ it('should export uninstall command', () => {
6
+ expect(uninstallCommands).toHaveLength(1);
7
+ expect(uninstallCommands[0].name).toBe('uninstall');
8
+ });
9
+
10
+ it('should have correct command definition', () => {
11
+ const cmd = uninstallCommands[0];
12
+ expect(cmd.description).toContain('Remove Bluera Knowledge data');
13
+ expect(cmd.argsSchema).toBeDefined();
14
+ expect(cmd.handler).toBeTypeOf('function');
15
+ });
16
+
17
+ it('should have valid schema for global and keepDefinitions options', () => {
18
+ const cmd = uninstallCommands[0];
19
+ const schema = cmd.argsSchema;
20
+
21
+ // Validate the schema accepts expected args
22
+ const result = schema?.safeParse({
23
+ global: true,
24
+ keepDefinitions: false,
25
+ });
26
+ expect(result?.success).toBe(true);
27
+ });
28
+
29
+ it('should have optional args in schema', () => {
30
+ const cmd = uninstallCommands[0];
31
+ const schema = cmd.argsSchema;
32
+
33
+ // Empty args should be valid
34
+ const result = schema?.safeParse({});
35
+ expect(result?.success).toBe(true);
36
+ });
37
+ });
@@ -0,0 +1,29 @@
1
+ import { z } from 'zod';
2
+ import { handleUninstall } from '../handlers/uninstall.handler.js';
3
+ import type { CommandDefinition } from './registry.js';
4
+ import type { UninstallArgs } from '../handlers/uninstall.handler.js';
5
+
6
+ /**
7
+ * Uninstall commands for removing Bluera Knowledge data
8
+ *
9
+ * Provides cleanup functionality for testing fresh installs or removing the plugin.
10
+ */
11
+ /* eslint-disable @typescript-eslint/consistent-type-assertions */
12
+ export const uninstallCommands: CommandDefinition[] = [
13
+ {
14
+ name: 'uninstall',
15
+ description: 'Remove Bluera Knowledge data from project (and optionally global data)',
16
+ argsSchema: z.object({
17
+ global: z
18
+ .boolean()
19
+ .optional()
20
+ .describe('Also remove global data (~/.local/share/bluera-knowledge)'),
21
+ keepDefinitions: z
22
+ .boolean()
23
+ .optional()
24
+ .describe('Keep stores.config.json for team sharing (default: true)'),
25
+ }),
26
+ handler: (args, context) => handleUninstall(args as unknown as UninstallArgs, context),
27
+ },
28
+ ];
29
+ /* eslint-enable @typescript-eslint/consistent-type-assertions */
@@ -0,0 +1,194 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
+ import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'fs';
3
+ import { join } from 'path';
4
+ import { tmpdir } from 'os';
5
+ import { handleUninstall } from './uninstall.handler.js';
6
+ import type { HandlerContext } from '../types.js';
7
+ import type { UninstallArgs } from './uninstall.handler.js';
8
+
9
+ // Mock os module to control homedir for testing global data deletion
10
+ vi.mock('node:os', async () => {
11
+ const actual = await vi.importActual<typeof import('node:os')>('node:os');
12
+ return {
13
+ ...actual,
14
+ homedir: vi.fn(() => actual.homedir()),
15
+ };
16
+ });
17
+
18
+ describe('uninstall.handler', () => {
19
+ let tempDir: string;
20
+ let projectRoot: string;
21
+ let mockContext: HandlerContext;
22
+
23
+ beforeEach(() => {
24
+ tempDir = mkdtempSync(join(tmpdir(), 'uninstall-handler-test-'));
25
+ projectRoot = tempDir;
26
+
27
+ // Create minimal mock context - uninstall handler doesn't use services
28
+ mockContext = {
29
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
30
+ services: {} as HandlerContext['services'],
31
+ options: { projectRoot },
32
+ };
33
+ });
34
+
35
+ afterEach(() => {
36
+ rmSync(tempDir, { recursive: true, force: true });
37
+ vi.restoreAllMocks();
38
+ });
39
+
40
+ describe('handleUninstall', () => {
41
+ it('should delete project data when it exists', async () => {
42
+ // Create project data structure
43
+ const bkDir = join(projectRoot, '.bluera', 'bluera-knowledge');
44
+ const dataDir = join(bkDir, 'data');
45
+ mkdirSync(dataDir, { recursive: true });
46
+ writeFileSync(join(bkDir, 'config.json'), '{}');
47
+ writeFileSync(join(dataDir, 'stores.json'), '[]');
48
+
49
+ const args: UninstallArgs = {};
50
+ const result = await handleUninstall(args, mockContext);
51
+
52
+ // Verify deletion
53
+ expect(existsSync(join(bkDir, 'data'))).toBe(false);
54
+ expect(existsSync(join(bkDir, 'config.json'))).toBe(false);
55
+
56
+ // Verify response
57
+ expect(result.content[0].type).toBe('text');
58
+ expect(result.content[0].text).toContain('Deleted:');
59
+ expect(result.content[0].text).toContain('plugin cache');
60
+ });
61
+
62
+ it('should preserve stores.config.json by default', async () => {
63
+ // Create project data structure
64
+ const bkDir = join(projectRoot, '.bluera', 'bluera-knowledge');
65
+ const dataDir = join(bkDir, 'data');
66
+ mkdirSync(dataDir, { recursive: true });
67
+ writeFileSync(join(bkDir, 'config.json'), '{}');
68
+ writeFileSync(join(bkDir, 'stores.config.json'), '{"stores":[]}');
69
+ writeFileSync(join(dataDir, 'stores.json'), '[]');
70
+
71
+ const args: UninstallArgs = { keepDefinitions: true };
72
+ const result = await handleUninstall(args, mockContext);
73
+
74
+ // Verify stores.config.json is preserved
75
+ expect(existsSync(join(bkDir, 'stores.config.json'))).toBe(true);
76
+ expect(existsSync(join(bkDir, 'data'))).toBe(false);
77
+ expect(existsSync(join(bkDir, 'config.json'))).toBe(false);
78
+
79
+ // Verify response mentions preserved files
80
+ expect(result.content[0].text).toContain('Preserved:');
81
+ expect(result.content[0].text).toContain('stores.config.json');
82
+ });
83
+
84
+ it('should delete stores.config.json when keepDefinitions is false', async () => {
85
+ // Create project data structure
86
+ const bkDir = join(projectRoot, '.bluera', 'bluera-knowledge');
87
+ mkdirSync(bkDir, { recursive: true });
88
+ writeFileSync(join(bkDir, 'stores.config.json'), '{"stores":[]}');
89
+
90
+ const args: UninstallArgs = { keepDefinitions: false };
91
+ const result = await handleUninstall(args, mockContext);
92
+
93
+ // Verify entire directory is gone
94
+ expect(existsSync(bkDir)).toBe(false);
95
+
96
+ // Verify response
97
+ expect(result.content[0].text).toContain('Deleted:');
98
+ expect(result.content[0].text).not.toContain('Preserved:');
99
+ });
100
+
101
+ it('should handle missing project data gracefully', async () => {
102
+ const args: UninstallArgs = {};
103
+ const result = await handleUninstall(args, mockContext);
104
+
105
+ // Verify response indicates nothing to delete
106
+ expect(result.content[0].text).toContain('No data found to delete');
107
+ expect(result.content[0].text).toContain('plugin cache');
108
+ });
109
+
110
+ it('should always include plugin cache instructions', async () => {
111
+ const args: UninstallArgs = {};
112
+ const result = await handleUninstall(args, mockContext);
113
+
114
+ expect(result.content[0].text).toContain('To fully uninstall (clear plugin cache):');
115
+ expect(result.content[0].text).toContain('~/.claude/plugins/cache/bluera-knowledge-*');
116
+ });
117
+
118
+ it('should mention venv is cleaned with plugin cache', async () => {
119
+ const args: UninstallArgs = {};
120
+ const result = await handleUninstall(args, mockContext);
121
+
122
+ expect(result.content[0].text).toContain('Python venv');
123
+ expect(result.content[0].text).toContain('all dependencies');
124
+ });
125
+
126
+ it('should handle subdirectories in data folder', async () => {
127
+ // Create nested structure
128
+ const bkDir = join(projectRoot, '.bluera', 'bluera-knowledge');
129
+ const reposDir = join(bkDir, 'data', 'repos', 'test-repo');
130
+ mkdirSync(reposDir, { recursive: true });
131
+ writeFileSync(join(reposDir, 'file.txt'), 'test');
132
+
133
+ const args: UninstallArgs = {};
134
+ await handleUninstall(args, mockContext);
135
+
136
+ // Verify everything is deleted
137
+ expect(existsSync(join(bkDir, 'data'))).toBe(false);
138
+ });
139
+
140
+ it('should delete global data when global flag is true', async () => {
141
+ // Mock homedir to return our temp directory
142
+ const { homedir } = await import('node:os');
143
+ vi.mocked(homedir).mockReturnValue(tempDir);
144
+
145
+ // Create fake global data directory
146
+ const globalDir = join(tempDir, '.local', 'share', 'bluera-knowledge');
147
+ mkdirSync(globalDir, { recursive: true });
148
+ writeFileSync(join(globalDir, 'jobs.json'), '[]');
149
+
150
+ const args: UninstallArgs = { global: true };
151
+ const result = await handleUninstall(args, mockContext);
152
+
153
+ // Verify global data was deleted
154
+ expect(existsSync(globalDir)).toBe(false);
155
+ expect(result.content[0].text).toContain('Deleted:');
156
+ expect(result.content[0].text).toContain('.local/share/bluera-knowledge');
157
+ });
158
+
159
+ it('should handle global flag when global data does not exist', async () => {
160
+ // Mock homedir to return our temp directory (no global data exists)
161
+ const { homedir } = await import('node:os');
162
+ vi.mocked(homedir).mockReturnValue(tempDir);
163
+
164
+ const args: UninstallArgs = { global: true };
165
+ const result = await handleUninstall(args, mockContext);
166
+
167
+ // The handler should still complete without errors
168
+ expect(result.content[0].text).toContain('plugin cache');
169
+ });
170
+
171
+ it('should report global data deletion in result', async () => {
172
+ // Mock homedir to return our temp directory
173
+ const { homedir } = await import('node:os');
174
+ vi.mocked(homedir).mockReturnValue(tempDir);
175
+
176
+ // Create both project and global data
177
+ const bkDir = join(projectRoot, '.bluera', 'bluera-knowledge');
178
+ mkdirSync(bkDir, { recursive: true });
179
+ writeFileSync(join(bkDir, 'config.json'), '{}');
180
+
181
+ const globalDir = join(tempDir, '.local', 'share', 'bluera-knowledge');
182
+ mkdirSync(globalDir, { recursive: true });
183
+ writeFileSync(join(globalDir, 'jobs.json'), '[]');
184
+
185
+ const args: UninstallArgs = { global: true, keepDefinitions: false };
186
+ const result = await handleUninstall(args, mockContext);
187
+
188
+ // Verify both were deleted
189
+ expect(existsSync(bkDir)).toBe(false);
190
+ expect(existsSync(globalDir)).toBe(false);
191
+ expect(result.content[0].text).toContain('Deleted:');
192
+ });
193
+ });
194
+ });